Skip to content

security: publish_url con userinfo (user:pass@) se filtra a la salida, a los ficheros publicados y a las credenciales firmadas #253

Description

@luisgf

Horizon: now · Effort: S · security hardening

A publish_url carrying userinfo (https://user:password@host/) leaks the credential into user-visible output, publicly served files and signed credentials. The only guard that exists today covers one narrow path, and that guard leaks the password itself.

1. did_web_from_url echoes the password it exists to reject

ob3/did.py:289-296. The docstring says the userinfo check is there so a user:pass@ credential does not reach the DID embedded in every issued credential — but the checks run in the wrong order:

if parts.scheme != 'https':
    raise ValueError('did:web requires an https URL, got %r' % (url,))   # raw url
if parts.username or parts.password:
    raise ValueError('did:web URL must not contain userinfo (user:pass@)')
if not parts.hostname:
    raise ValueError('URL %r has no host' % (url,))                      # raw url

http://user:password@host/ therefore prints the password in the error — which the CLI writes to stdout and into the error field of --json. The rejection publishes the credential.

2. The wider hole: the guard almost never runs

did_web_from_url is reached only when the issuer DID is derived ([issuer] did = auto, confparser.py:94ob3/publish.py:360). With an explicit did = did:web:..., or when publishing OB1/OB2, nothing validates publish_url and it flows unsanitized into:

  • stdout on the success paths of publish — openbadges_publish.py:394-395 (OB3), 546-547 (OB2), 602-603 (OB1), 407 (--check-live);
  • files meant to be served publiclyopenbadges_publish.py:484-522 builds ids with urljoin(publish_url, ...) (urljoin preserves user:pass@) and writes them into organization.json, badge_*/badge.json and key.json;
  • signed OB3 credentials delivered to the recipientconfparser.py:189-193 derives status_base/list URLs from publish_url, and issue.py:496 puts them in credentialStatus.statusListCredential.

The same applies to the other public identifiers in those sections ([issuer] url — the OB3 issuer-id fallback — image, and a badge's criteria / status_base / hosted_assertions_base).

Action

Reject userinfo once, at config-parse time in confparser.py, so every consumer (OB1/OB2/OB3 publishing, DID derivation, status lists, published files) inherits the guarantee instead of re-checking it. The ConfigError must name the section and key and must not echo the value. [smtp] username/password stay untouched — they never leave the issuer's machine.

Keep the did.py check as defence in depth, but fix its ordering and stop interpolating the raw URL in the other two messages.

Tests: a config with https://user:pass@host/ must be rejected, and no error message anywhere may contain the password.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions