Skip to content

Commit 1660728

Browse files
authored
fix: only percent-encode characters in the userinfo encode set (#1852)
1 parent e1aa528 commit 1660728

File tree

2 files changed

+50
-15
lines changed

2 files changed

+50
-15
lines changed

src/url.rs

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use std::sync::OnceLock;
77

88
use idna::punycode::decode_to_string;
99
use jiter::{PartialMode, StringCacheMode};
10-
use percent_encoding::{percent_encode, NON_ALPHANUMERIC};
10+
use percent_encoding::{percent_encode, AsciiSet, CONTROLS};
1111
use pyo3::exceptions::PyValueError;
1212
use pyo3::pyclass::CompareOp;
1313
use pyo3::sync::OnceLockExt;
@@ -602,8 +602,36 @@ fn is_punnycode_domain(lib_url: &Url, domain: &str) -> bool {
602602
scheme_is_special(lib_url.scheme()) && domain.split('.').any(|part| part.starts_with(PUNYCODE_PREFIX))
603603
}
604604

605+
/// See <https://url.spec.whatwg.org/#userinfo-percent-encode-set>
606+
const USERINFO_ENCODE_SET: &AsciiSet = &CONTROLS
607+
// query percent-encodes is controls plus the below
608+
.add(b' ')
609+
.add(b'"')
610+
.add(b'#')
611+
.add(b'<')
612+
.add(b'>')
613+
// path percent-encodes is query percent-encodes plus the below
614+
.add(b'?')
615+
.add(b'^')
616+
.add(b'`')
617+
.add(b'{')
618+
.add(b'}')
619+
// userinfo percent-encodes is path percent-encodes plus the below
620+
.add(b'/')
621+
.add(b':')
622+
.add(b';')
623+
.add(b'=')
624+
.add(b'@')
625+
.add(b'[')
626+
.add(b'\\')
627+
.add(b']')
628+
.add(b'|')
629+
// https://datatracker.ietf.org/doc/html/rfc3986.html#section-2.4
630+
// we must also percent-encode '%'
631+
.add(b'%');
632+
605633
fn encode_userinfo_component(value: &str) -> Cow<'_, str> {
606-
let encoded = percent_encode(value.as_bytes(), NON_ALPHANUMERIC).to_string();
634+
let encoded = percent_encode(value.as_bytes(), USERINFO_ENCODE_SET).to_string();
607635
if encoded == value {
608636
Cow::Borrowed(value)
609637
} else {

tests/validators/test_url.py

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1318,31 +1318,38 @@ def test_multi_url_build() -> None:
13181318
assert str(url) == 'postgresql://testuser:testpassword@127.0.0.1:5432/database?sslmode=require#test'
13191319

13201320

1321-
def test_multi_url_build_encodes_credentials() -> None:
1322-
url = MultiHostUrl.build(
1321+
@pytest.mark.parametrize('url_type', [Url, MultiHostUrl])
1322+
def test_url_build_encodes_credentials(url_type: type[Union[Url, MultiHostUrl]]) -> None:
1323+
url = url_type.build(
13231324
scheme='postgresql',
13241325
username='user name',
1325-
password='p@ss/word?#',
1326+
password='p@ss/word?#__',
13261327
host='example.com',
13271328
port=5432,
13281329
)
1329-
assert url == MultiHostUrl('postgresql://user%20name:p%40ss%2Fword%3F%23@example.com:5432')
1330-
assert str(url) == 'postgresql://user%20name:p%40ss%2Fword%3F%23@example.com:5432'
1331-
assert url.hosts() == [
1332-
{'username': 'user%20name', 'password': 'p%40ss%2Fword%3F%23', 'host': 'example.com', 'port': 5432}
1333-
]
1330+
assert url == url_type('postgresql://user%20name:p%40ss%2Fword%3F%23__@example.com:5432')
1331+
assert str(url) == 'postgresql://user%20name:p%40ss%2Fword%3F%23__@example.com:5432'
1332+
if url_type is Url:
1333+
assert url.username == 'user%20name'
1334+
assert url.password == 'p%40ss%2Fword%3F%23__'
1335+
else:
1336+
assert url.hosts() == [
1337+
{'username': 'user%20name', 'password': 'p%40ss%2Fword%3F%23__', 'host': 'example.com', 'port': 5432}
1338+
]
13341339

13351340

13361341
def test_multi_url_build_hosts_encodes_credentials() -> None:
13371342
hosts = [
1338-
{'host': 'example.com', 'password': 'p@ss/word?#', 'username': 'user name', 'port': 5431},
1339-
{'host': 'example.org', 'password': 'pa%ss', 'username': 'other', 'port': 5432},
1343+
{'host': 'example.com', 'password': 'p@ss/word?#__', 'username': 'user name', 'port': 5431},
1344+
{'host': 'example.org', 'password': 'p@%ss__', 'username': 'other', 'port': 5432},
13401345
]
13411346
url = MultiHostUrl.build(scheme='postgresql', hosts=hosts)
1342-
assert str(url) == 'postgresql://user%20name:p%40ss%2Fword%3F%23@example.com:5431,other:pa%25ss@example.org:5432'
1347+
assert (
1348+
str(url) == 'postgresql://user%20name:p%40ss%2Fword%3F%23__@example.com:5431,other:p%40%25ss__@example.org:5432'
1349+
)
13431350
assert url.hosts() == [
1344-
{'username': 'user%20name', 'password': 'p%40ss%2Fword%3F%23', 'host': 'example.com', 'port': 5431},
1345-
{'username': 'other', 'password': 'pa%25ss', 'host': 'example.org', 'port': 5432},
1351+
{'username': 'user%20name', 'password': 'p%40ss%2Fword%3F%23__', 'host': 'example.com', 'port': 5431},
1352+
{'username': 'other', 'password': 'p%40%25ss__', 'host': 'example.org', 'port': 5432},
13461353
]
13471354

13481355

0 commit comments

Comments
 (0)