Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code 2ad9a20cc5 fix(twenty-shared): stop decoding URI components in URL normalization
https://sonarly.com/issue/15564?type=bug

The `lowercaseUrlOriginAndRemoveTrailingSlash` function applies `decodeURIComponent` to URL pathname and search components, converting intentionally encoded characters like `%2F` to `/`, which breaks URLs that depend on encoding (e.g., Google Maps).

Fix: Removed `safeDecodeURIComponent()` wrapping from `url.pathname` and `url.search` in `lowercaseUrlOriginAndRemoveTrailingSlash`. This function was added in commit `1119e3d77e` to handle accented characters, but `new URL()` already normalizes non-ASCII characters in pathnames (encoding `é` as `%C3%A9`). The `decodeURIComponent` call was incorrectly decoding structurally significant percent-encoded characters like `%2F` (encoded slash) back to `/`, breaking URLs that depend on encoding (e.g., Google Maps `data=` parameter).

The fix restores the original `url.pathname + url.search + url.hash` concatenation, which correctly preserves all percent-encoding as provided by the URL constructor. The `safeDecodeURIComponent` utility itself is kept since it's still used by the IMAP message text extractor service.

Updated test expectations to match correct URL normalization behavior:
- Percent-encoded characters like `%2F`, `%20`, `%2520` are preserved as-is
- Non-ASCII characters are encoded by the URL constructor (e.g., `é` → `%C3%A9`)
- Added a Google Maps URL test case to prevent regression
2026-03-17 11:50:20 +00:00
2 changed files with 22 additions and 17 deletions
@@ -39,19 +39,19 @@ describe('lowercaseUrlOriginAndRemoveTrailingSlash', () => {
expected: 'https://www.example.com/TEST#Hash',
},
{
title: 'should preserve special characters in path',
title: 'should encode special characters in path via URL normalization',
input: 'https://test.test/edouard-ménard-22219837',
expected: 'https://test.test/edouard-ménard-22219837',
expected: 'https://test.test/edouard-m%C3%A9nard-22219837',
},
{
title: 'should decode already encoded special characters in path',
title: 'should preserve already encoded special characters in path',
input: 'https://test.test/edouard-m%C3%A9nard-22219837',
expected: 'https://test.test/edouard-ménard-22219837',
expected: 'https://test.test/edouard-m%C3%A9nard-22219837',
},
{
title: 'should preserve special characters in query params',
title: 'should encode special characters in query params',
input: 'https://example.com/path?name=José',
expected: 'https://example.com/path?name=José',
expected: 'https://example.com/path?name=Jos%C3%A9',
},
{
title:
@@ -60,13 +60,12 @@ describe('lowercaseUrlOriginAndRemoveTrailingSlash', () => {
expected: 'https://example.com/test%E0%A4%A',
},
{
title:
'should preserve double-encoded URLs (encoded percent signs stay encoded once)',
title: 'should preserve double-encoded URLs without decoding them',
input: 'https://example.com/test%2520name',
expected: 'https://example.com/test%20name',
expected: 'https://example.com/test%2520name',
},
{
title: 'should preserve special characters in hash fragments',
title: 'should encode special characters in hash fragments',
input: 'https://example.com/path#frédéric',
expected: 'https://example.com/path#fr%C3%A9d%C3%A9ric',
},
@@ -76,9 +75,19 @@ describe('lowercaseUrlOriginAndRemoveTrailingSlash', () => {
expected: 'https://example.com/path#fr%C3%A9d%C3%A9ric',
},
{
title: 'should handle mixed encoded and non-encoded in same URL',
title:
'should preserve percent-encoded slashes and query params without decoding',
input: 'https://example.com/path%2Fwith%2Fslashes?query=hello%20world',
expected: 'https://example.com/path/with/slashes?query=hello world',
expected:
'https://example.com/path%2Fwith%2Fslashes?query=hello%20world',
},
{
title:
'should preserve percent-encoded characters in Google Maps URLs',
input:
'https://www.google.com/maps/place/Test/data=!16s%2Fg%2F1ptwh8096',
expected:
'https://www.google.com/maps/place/Test/data=!16s%2Fg%2F1ptwh8096',
},
])('$title', ({ input, expected }) => {
expect(lowercaseUrlOriginAndRemoveTrailingSlash(input)).toBe(expected);
@@ -1,6 +1,5 @@
import { getURLSafely } from '@/utils/getURLSafely';
import { isDefined } from '@/utils/validation';
import { safeDecodeURIComponent } from './safeDecodeURIComponent';
export const lowercaseUrlOriginAndRemoveTrailingSlash = (rawUrl: string) => {
const url = getURLSafely(rawUrl);
@@ -10,10 +9,7 @@ export const lowercaseUrlOriginAndRemoveTrailingSlash = (rawUrl: string) => {
}
const lowercaseOrigin = url.origin.toLowerCase();
const path =
safeDecodeURIComponent(url.pathname) +
safeDecodeURIComponent(url.search) +
url.hash;
const path = url.pathname + url.search + url.hash;
return (lowercaseOrigin + path).replace(/\/$/, '');
};