diff --git a/changedetectionio/tests/unit/test_validate_url.py b/changedetectionio/tests/unit/test_validate_url.py new file mode 100644 index 00000000..616413b8 --- /dev/null +++ b/changedetectionio/tests/unit/test_validate_url.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 + +# run from dir above changedetectionio/ dir +# python3 -m unittest changedetectionio.tests.unit.test_validate_url + +import unittest + +from changedetectionio.validate_url import is_safe_valid_url, normalize_url_encoding + + +class TestValidateUrl(unittest.TestCase): + + def test_fragment_with_pipe_characters_is_valid(self): + """https://github.com/dgtlmoon/changedetection.io/issues/4209 + Browsers accept '|' in the fragment/anchor but RFC 3986 does not, + so it must be percent-encoded before validation instead of rejected.""" + url = 'https://www.fnac.com/Pack-Tablette-Tactile-Samsung-Galaxy-Tab-S10-Lite-10-9-Wi-Fi-128-Go-Anthracite-Book-Cover/a21903823/w-4#int=S:PFreco|PF|48966|21903823|BL4|L1' + self.assertTrue(is_safe_valid_url(url), f"URL '{url}' with '|' in the fragment should be valid") + + def test_fragment_pipe_is_percent_encoded(self): + normalized = normalize_url_encoding('https://example.com/page#a|b') + self.assertEqual(normalized, 'https://example.com/page#a%7Cb') + + def test_already_encoded_fragment_is_not_double_encoded(self): + normalized = normalize_url_encoding('https://example.com/page#a%7Cb') + self.assertEqual(normalized, 'https://example.com/page#a%7Cb') + + def test_fragment_normalization_is_idempotent(self): + url = 'https://example.com/page#int=S:PFreco|PF|48966' + once = normalize_url_encoding(url) + twice = normalize_url_encoding(once) + self.assertEqual(once, twice) + + def test_rfc3986_fragment_characters_are_untouched(self): + # pchar / "/" / "?" are all legal in a fragment and must survive as-is + urls = [ + 'https://example.com/docs#section-2.1', + 'https://example.com/app#/route/sub?tab=1', + "https://example.com/page#key=val&other=a:b@c!$&'()*+,;=", + ] + for url in urls: + with self.subTest(url=url): + self.assertEqual(normalize_url_encoding(url), url) + self.assertTrue(is_safe_valid_url(url)) + + def test_unsafe_urls_are_still_rejected(self): + # The fragment fix must not loosen any of the existing security checks + unsafe = [ + 'javascript:alert(1)', + 'source:javascript:alert(1)', + 'https://example.com/page#