redis: send percent-decoded URL credentials byte for byte
RO
2mo ago+93+93+01234567890123456789-47-47-01234567890123456789
Problem
new RedisClient("redis://jos%E9:pw@host")sendsHELLO 3 AUTH default pw: a different ACL user with the same password.redis://user:p%E9ss@hostsendsAUTH user "". Any userinfo whose percent-decoded bytes are not UTF-8 is silently replaced, with no client-side error.- The cause is
URL__username/URL__password(src/jsc/bindings/BunString.cpp:690). They callWTF::URL::user()/password(), which percent-decode and thenString::fromUTF8()the bytes. That returns a null string for invalid UTF-8. The client then sees an empty username and falls back todefault(valkey.rsauthenticate()).
Fix
URL__username/URL__passwordnow returnencodedUser()/encodedPassword(): the userinfo as written, still percent-encoded, the same as JSurl.username. The Redis client is their only caller.- The client percent-decodes that to raw bytes with a new
PercentEncoding::decode_whatwg(src/url/lib.rs) and sends those bytes inHELLO ... AUTH.HELLOandAUTHarguments are bulk strings, so any byte sequence is valid on the wire. - A
%that is not followed by two hex digits (%zz) stays literal. This is the WHATWG percent-decode rule and the behavior before this change. - Verified:
test/js/valkey/valkey.test.ts("sends the percent-decoded credentials ... byte for byte", 3 of 6 rows fail on 1.4.3). Also ranvalkey-incremental-scan.test.tsandreliability/connection-failures.test.ts.
Background
- A Redis URL carries credentials in the userinfo:
redis://user:pass@host:port/db. Reserved or non-ASCII bytes are written as%XX. Percent-encoding encodes octets, not characters, so%E9is the single byte0xE9(Latin-1é), which is not valid UTF-8 on its own. - On connect the client sends
HELLO 3 AUTH <user> <pass>as a RESP array of bulk strings ($<len>\r\n<bytes>\r\n). Bulk strings are length-prefixed, so they are binary-safe. bun_url::whatwg::URLis a thin Rust handle over WebKit'sWTF::URL. Each getter is oneextern "C"function inBunString.cpp.
Notes
- node-redis 5.12.1 and ioredis 5.11.1 throw
URIError: URI malformedat construction for%E9,%ff%feand%zz, because they rundecodeURIComponenton the userinfo. Sending the raw bytes was chosen over rejecting: Redis ACL passwords are byte strings, the URL states those bytes without ambiguity, and this adds no new throw path. A Latin-1étyped by mistake now getsWRONGPASSfrom the server instead of a silent login asdefault. ValkeyClient::connection_stringsis removed. It was a second copy of username + password + hostname that nothing read (each field is already its ownBox<[u8]>), and the block that built it is the one this change rewrites.- Wire output of the report's repro after the change:
redis://jos%C3%A9:pw@ -> "HELLO" "3" "AUTH" <hex 6a6f73c3a9> "pw"
redis://jos%E9:pw@ -> "HELLO" "3" "AUTH" <hex 6a6f73e9> "pw"
redis://user:p%E9ss@ -> "HELLO" "3" "AUTH" "user" <hex 70e97373>
redis://user:%ff%fe@ -> "HELLO" "3" "AUTH" "user" <hex fffe>
redis://:%zz@ -> "HELLO" "3" "AUTH" "default" "%zz"