Skip to content

security(config): refuse any '@' in a published URL, not just userinfo - #256

Merged
luisgf merged 1 commit into
masterfrom
autofix/friendly-feistel-12207b
Jul 22, 2026
Merged

security(config): refuse any '@' in a published URL, not just userinfo#256
luisgf merged 1 commit into
masterfrom
autofix/friendly-feistel-12207b

Conversation

@luisgf

@luisgf luisgf commented Jul 22, 2026

Copy link
Copy Markdown
Owner

Closes #255.

The guard from #254 could be bypassed: it looked for an @ in
urlsplit(...).netloc, but urlsplit only reports userinfo when the @ sits
inside the authority. A password holding a /, ? or # pushes the marker
into the path, and both layers waved the value through.

Verified end to end against 0954e9d before the fix:

publish_url config result
https://user:12345/x@host/ accepted issuer id = did:web:user%3A12345:x%40host
https://user:secret/x@host/ accepted ValueError: Port could not be cast to integer value as 'secret'

So the credential went into the DID carried by every signed credential, or
into an error message on stdout and the --json error field. That second
one is raised by urllib, so the redaction #254 added — which only covers our own
messages — never reached it.

The rule

A published identifier has no legitimate use for an @, so both layers now
refuse one anywhere in a URL-shaped value (%40 included) instead of
parsing out a userinfo component a malformed URL can hide. A value with no
authority is left alone, so [issuer] email and local paths keep their @.

did_web_from_url also wraps the parts.port parse and quotes nothing at all —
not even a redacted copy, since on a schemeless user:password@host/ urlsplit
reads the username as the scheme.

BREAKING (narrow): a publish_url whose path contains an @, e.g.
https://host/@name/, is now refused. Once parsed it is indistinguishable from
one hiding a credential.

Tests

17 new assertions: the five bypass shapes at both layers, the invalid-port
message, end to end through publish -V 1/2/3, and a pin that an address or
local path with an @ still loads. All 17 fail on 0954e9d.

flake8 clean, mypy clean, 1326 tests pass.

The guard added in 0954e9d could be bypassed. It asked urlsplit for the
authority and looked for an '@' there, but urlsplit only reports userinfo
when the '@' sits inside the authority: a password holding a '/', '?' or
'#' pushes the marker into the path, query or fragment, and both layers
then waved the value through.

Two ways the credential resurfaced, verified end to end against 0954e9d:

  publish_url = https://user:12345/x@host/
    -> config accepted, issuer id = did:web:user%3A12345:x%40host
       (the credential percent-encoded into the DID that travels in
       `issuer` on every signed credential — exactly what the guard
       existed to prevent)

  publish_url = https://user:secret/x@host/
    -> config accepted, then ValueError: Port could not be cast to
       integer value as 'secret'
       (urllib raises that one, so the redaction added in 0954e9d — which
       only covers our own messages — never touched it; it reaches stdout
       and the --json `error` field)

A published identifier has no legitimate use for an '@', so both layers
now refuse one anywhere in a URL-shaped value, '%40' included, instead of
parsing out a userinfo component that a malformed URL can hide. A value
with no authority is left alone, so [issuer] email and local paths keep
their '@'. did_web_from_url additionally wraps the parts.port parse (its
ValueError quotes the value) and no longer quotes the URL in any message —
not even redacted, since on a schemeless 'user:password@host/' urlsplit
reads the username as the scheme.

BREAKING (narrow): a publish_url whose path contains an '@', such as
https://host/@name/, is now refused. Once parsed, that shape is
indistinguishable from one hiding a credential; the blunt rule wins.

Tests cover the five bypass shapes at both layers plus end to end through
publish -V 1/2/3, assert no message quotes the value, and pin that an
address or local path with an '@' still loads.

Closes #255.
@luisgf
luisgf merged commit 88fa43a into master Jul 22, 2026
13 of 14 checks passed
@luisgf
luisgf deleted the autofix/friendly-feistel-12207b branch July 22, 2026 20:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

security: el guardián de userinfo se salta con un '/' en la contraseña (bypass de 0954e9d)

1 participant