Skip to content

fix: harden OCI Bearer authentication against SSRF and credential forwarding - #1276

Open
Kaniska (Kaniska244) wants to merge 6 commits into
devcontainers:mainfrom
Kaniska244:security-issue-analysis
Open

fix: harden OCI Bearer authentication against SSRF and credential forwarding#1276
Kaniska (Kaniska244) wants to merge 6 commits into
devcontainers:mainfrom
Kaniska244:security-issue-analysis

Conversation

@Kaniska244

@Kaniska244 Kaniska (Kaniska244) commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR hardens OCI Feature and Template authentication against registry-controlled Bearer token endpoints. The origin of this PR is this security issue.

Previously, an OCI registry could return a WWW-Authenticate: Bearer challenge containing an arbitrary realm. The CLI would request that URL without validating its scheme or authority, potentially:

  • Issuing requests to loopback, link-local, private-network, or cloud metadata endpoints.
  • Sending locally stored Basic credentials or OAuth refresh tokens to an attacker-selected endpoint.
  • Following redirects from an initially accepted token endpoint.
  • Allowing service and scope values to inject additional query parameters or fragments.
  • Returning a token-shaped response to the attacker-controlled registry as a Bearer credential.

This PR validates Bearer realms before credential lookup or token endpoint I/O, restricts credential forwarding separately from endpoint admission, disables token-request redirects, encodes challenge parameters, and validates OCI registry authority syntax.

Security classification

  • CWE-918: Server-Side Request Forgery, client-side variant
  • CWE-200: Exposure of Sensitive Information
  • CWE-522: Insufficiently Protected Credentials
  • CWE-319: Cleartext Transmission of Sensitive Information

The vulnerable flow can be reached through Feature or Template identifiers supplied by an untrusted workspace's devcontainer.json.

Threat model

A victim opens or builds an untrusted repository containing an attacker-selected OCI Feature or Template reference.

The Dev Container CLI may run directly through commands such as devcontainer up and devcontainer build, or transitively through the VS Code Dev Containers extension, Codespaces, or prebuild tooling.

The attacker controls the registry response and its WWW-Authenticate challenge. Before this change, that challenge could direct the victim's machine to an arbitrary Bearer token endpoint.

Changes

Validate Bearer token realms

requestEnsureAuthenticated() now validates the challenge realm before:

  • Looking up registry credentials.
  • Sending Basic credentials.
  • Sending a refresh token.
  • Making any token endpoint request.

Malformed, relative, or disallowed realms terminate authentication without contacting the token endpoint.

The following realm policy is applied:

Realm Result
HTTPS with the exact registry authority, including port Allowed
HTTP with the exact localhost authority and matching port Allowed
https://auth.docker.io without a custom port Allowed
https://ghcr.io without a custom port Allowed
Proper *.azurecr.io HTTPS authority without a custom port Allowed
Remote HTTP endpoint Rejected
HTTP IP literal Rejected
Different localhost port Rejected
Arbitrary cross-authority HTTPS endpoint Rejected
Relative or malformed URL Rejected
Known service with a custom port Rejected
Forged suffix such as auth.docker.io.attacker.example Rejected
Bare or malformed Azure authority Rejected

Authority comparisons are case-insensitive, and ports remain part of exact-authority matching.

Separate endpoint admission from credential forwarding

Token endpoint admission and credential forwarding are intentionally separate policies.

An endpoint may be safe to contact anonymously without being allowed to receive credentials stored for the registry. This prevents an attacker-controlled registry from naming a known token service and causing credentials for the attacker registry to be forwarded there.

Basic credentials and refresh tokens may be sent only when:

  • The realm uses HTTPS and exactly matches the registry authority.
  • The realm uses HTTP on exact same-authority localhost, including the same port.

Docker Hub requires a compatibility exception because its registries authenticate through a separate authority:

  • Basic credentials may be sent from docker.io, registry.docker.io, or registry-1.docker.io to https://auth.docker.io.
  • Refresh tokens are not forwarded across that authority boundary.
  • Custom ports are not accepted for the Docker Hub token service.

When credentials cannot be forwarded, the CLI logs a warning and attempts anonymous token acquisition.

Rebuild anonymous fallback requests

A credentialed token request that receives 401 or 403 is retried anonymously.

The anonymous retry is constructed as a fresh GET request rather than reusing the credentialed request. This ensures that neither of the following can be replayed:

  • A Basic Authorization header.
  • A refresh-token POST body.

Anonymous requests are not redundantly retried after 401 or 403.

Disable redirects for Bearer token requests

A dedicated requestResolveHeadersNoRedirects() wrapper now sends Bearer token requests with maxRedirects: 0.

This applies to:

  • Basic-authenticated token GET requests.
  • Refresh-token POST requests.
  • Anonymous token GET requests.
  • Anonymous fallback requests.

This prevents an accepted token endpoint from redirecting the request to another authority.

Normal HTTP operations retain their existing redirect behavior. Redirect suppression is limited to token requests whose validated authority forms a security boundary.

Encode service and scope

Bearer token GET requests now use URL and URL.searchParams.set() instead of interpolating challenge values into a URL string.

This:

  • Percent-encodes service and scope.
  • Prevents query parameter injection.
  • Prevents fragment injection through challenge values.
  • Preserves unrelated query parameters already present in the realm.
  • Replaces existing service and scope parameters deterministically.

Refresh-token POST bodies continue to use URLSearchParams.

Validate registry authority syntax

getRef() and getCollectionRef() now validate registry authority syntax before constructing an OCI reference.

Accepted registry forms include:

  • Valid DNS hostnames.
  • Syntactically valid single-label authorities.
  • IPv4 literals.
  • Bracketed IPv6 literals.
  • Optional numeric ports from 1 through 65535.
  • Local registries such as localhost:5000.

Rejected forms include:

  • Schemes such as https://.
  • User information such as user@registry.
  • Path, query, or fragment-shaped authorities.
  • Empty or malformed DNS labels.
  • DNS labels beginning or ending with -.
  • Unbracketed IPv6 literals.
  • Missing, non-numeric, zero, or out-of-range ports.

This validation checks authority shape only. It does not impose a global network trust policy on user-configured registries.

Preserve legacy Feature resolution

The original implementation contained an early check that prevented single-label identifiers from being resolved as OCI registries. Removing it caused legacy Feature identifiers such as:

codspace/myfeatures/helloworld

to trigger a request to:

https://codspace/v2/myfeatures/helloworld/manifests/latest

This caused existing tests and configurations to fail with errors such as:

getaddrinfo EAI_AGAIN codspace

The compatibility behavior has therefore been restored at manifest resolution time.

Single-label references continue to fall back to the legacy GitHub Releases Feature flow, except for explicitly supported local or IP-literal registries:

  • localhost
  • IPv4 literals
  • Bracketed IPv6 literals

The authority parser can still validate single-label authorities, but OCI manifest probing preserves the historical interpretation of ambiguous three-segment Feature identifiers.

Remove the MCR substring workaround

The previous special case:

if (realm.includes('mcr.microsoft.com')) {
    return undefined;
}

has been removed.

Substring matching is not a valid security boundary and could also match attacker-controlled hosts such as:

mcr.microsoft.com.attacker.example

MCR realms are now governed by the same parsed URL, protocol, authority, redirect, and credential-forwarding policies as other registries.

Deviations from the original remediation

Exact-authority localhost HTTP remains supported

The original recommendation proposed rejecting every non-HTTPS realm and requiring an explicit opt-in for localhost.

This PR retains HTTP only when:

  • The hostname is exactly localhost.
  • The token realm authority exactly matches the registry authority.
  • The port matches.

This preserves established local Feature and Template development workflows using registries such as:

http://localhost:<port>

The exception does not permit:

  • Remote HTTP registries.
  • HTTP IP-literal realms.
  • A different localhost port.
  • Cross-authority HTTP token services.

Local, private, and IP-literal registries remain supported

The original recommendation proposed rejecting loopback, private-network, and IP-literal registries at the source.

This PR intentionally performs syntax validation rather than globally blocking those registries because the CLI supports:

  • Local development registries.
  • On-premises registries.
  • Private-network registries.
  • IPv4 registries.
  • Bracketed IPv6 registries.

Rejecting these authorities globally would be a breaking policy change. It would also not completely prevent DNS-based SSRF without resolving addresses and enforcing network policy across every connection and redirect hop.

The fix is instead applied at the vulnerable secondary-request boundary: a registry-controlled Bearer challenge cannot pivot token acquisition to an arbitrary internal or attacker-selected authority.

No --allow-insecure-registry option was introduced

The original recommendation suggested adding an explicit insecure-registry option.

This PR does not add a new CLI configuration surface. It preserves the existing exact-authority localhost workflow while rejecting remote HTTP Bearer realms.

A broader insecure-registry policy can be considered separately if explicit configuration becomes necessary.

Known token services remain available

The realm policy allows standard HTTPS endpoints used by Docker Hub, GHCR, and Azure Container Registry:

  • auth.docker.io
  • ghcr.io
  • Proper subdomains of azurecr.io

This follows the compatibility allowance in the original recommendation.

These endpoints may receive anonymous token requests, but credential forwarding is evaluated independently. An attacker-controlled registry therefore cannot use the endpoint allow-list to send its stored credentials to one of these services.

Registry validation does not reject every single-label authority

The authority validator accepts syntactically valid single-label names because they can represent valid local or enterprise authorities.

However, during Feature manifest probing, ambiguous single-label identifiers continue to use the historical legacy GitHub Releases fallback. This avoids breaking existing Feature references such as codspace/myfeatures/helloworld.

Workspace trust behavior is unchanged

The original report suggested considering an explicit workspace-trust prompt before resolving Features from non-default registries.

This PR does not change workspace trust or introduce interactive prompts. Such a change would affect broader product behavior beyond OCI authentication and should be evaluated independently.

Tests

A focused OCI authentication suite was added covering:

  • Exact registry authority matching.
  • Case-insensitive host matching.
  • Port matching and mismatching.
  • HTTPS enforcement.
  • Exact-authority localhost HTTP compatibility.
  • Localhost port isolation.
  • Rejection of malformed and relative realms.
  • Docker Hub token endpoint handling.
  • GHCR token endpoint handling.
  • Azure Container Registry endpoint handling.
  • Rejection of forged hostname suffixes.
  • Rejection of custom ports on known token services.
  • Rejection of loopback and metadata-address HTTP realms.
  • Basic credential forwarding policy.
  • Refresh-token forwarding policy.
  • Docker Hub's Basic-only cross-authority exception.
  • Rejection before token endpoint I/O.
  • Prevention of token endpoint redirects.
  • Encoding of service and scope.
  • Preservation of existing realm query parameters.
  • Successful Bearer authentication after safe token acquisition.

OCI reference tests were extended to cover:

  • localhost registries.
  • IPv4 registries.
  • Bracketed IPv6 registries.
  • Schemes in registry authorities.
  • User information.
  • Malformed DNS labels.
  • Invalid and out-of-range ports.
  • Legacy single-label Feature identifiers.
  • Prevention of OCI DNS requests for codspace/myfeatures/helloworld.

CI integration

src/test/httpOCIRegistry.test.ts has a dedicated entry in the GitHub Actions test matrix.

It is excluded from the catch-all test entry so it runs exactly once and receives an independent matrix result.

The existing parser and legacy Feature regression tests continue to run through the catch-all test shard.

Validation

Local validation completed successfully:

  • OCI authentication suite: 36 passing.
  • Focused OCI reference and legacy fallback suite: 27 passing.
  • yarn type-check
  • yarn lint
  • yarn package
  • GitHub Actions workflow diagnostics

Files changed

  • src/spec-configuration/httpOCIRegistry.ts

    • Bearer realm validation.
    • Credential forwarding policy.
    • Safe anonymous fallback.
    • Structured token query construction.
    • Removal of the MCR substring workaround.
  • src/spec-utils/httpRequest.ts

    • Dedicated no-redirect request path for Bearer token exchanges.
  • src/spec-configuration/containerCollectionsOCI.ts

    • Registry authority syntax validation.
    • Legacy single-label Feature fallback.
    • Localhost and IP registry compatibility.
  • src/test/httpOCIRegistry.test.ts

    • Focused security and authentication tests.
  • src/test/container-features/containerFeaturesOCI.test.ts

    • Registry parser and legacy fallback regression tests.
  • .github/workflows/dev-containers.yml

    • Dedicated authentication test matrix entry.
    • Exclusion from the catch-all shard.

Security outcome

After this change, a malicious OCI registry cannot use its Bearer challenge to:

  • Send the CLI to an arbitrary loopback, link-local, private-network, metadata, or attacker-selected token endpoint.
  • Downgrade a remote token request to HTTP.
  • Redirect a validated token request to another authority.
  • Forward registry credentials to an unrelated token endpoint.
  • Replay a refresh-token POST body during anonymous fallback.
  • Inject additional token query parameters through service or scope.
  • Exploit the removed MCR substring special case.

The CLI continues to support legitimate local, private, Docker Hub, GHCR, Azure Container Registry, and legacy Feature workflows.

References

@Kaniska244 Kaniska (Kaniska244) changed the title fix: Security findings fix: harden OCI Bearer authentication against SSRF and credential forwarding Aug 11, 2026
@Kaniska244
Kaniska (Kaniska244) marked this pull request as ready for review August 11, 2026 09:28
@Kaniska244
Kaniska (Kaniska244) requested a review from a team as a code owner August 11, 2026 09:28

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens OCI Feature/Template registry authentication by validating Bearer realm challenges, restricting credential forwarding, preventing redirects during token exchange, encoding challenge parameters, and tightening registry authority parsing to mitigate SSRF and credential leakage.

Changes:

  • Added Bearer token realm admission rules and separated realm admission from credential-forwarding rules (including Docker Hub/GHCR/AzureCR allowances).
  • Disabled redirects specifically for Bearer token endpoint requests and rebuilt anonymous fallback requests to avoid replaying credentials.
  • Added registry authority syntax validation and restored legacy single-label Feature ID behavior to avoid unintended OCI probing.
Show a summary per file
File Description
src/spec-configuration/httpOCIRegistry.ts Adds Bearer realm validation, credential-forwarding policy, redirect-free token requests, and safer token request construction.
src/spec-utils/httpRequest.ts Introduces a no-redirect request helper for token endpoints via configurable redirect limits.
src/spec-configuration/containerCollectionsOCI.ts Validates registry authority syntax and preserves legacy single-label Feature fallback while allowing explicit localhost/IP registries.
src/test/httpOCIRegistry.test.ts Adds focused tests for realm admission, credential forwarding, redirect blocking, and query encoding.
src/test/container-features/containerFeaturesOCI.test.ts Extends tests for registry parsing/validation and legacy single-label behavior.
.github/workflows/dev-containers.yml Runs the new focused OCI auth test suite exactly once by adding it to the matrix and excluding it from the catch-all shard.

Review details

Tip

Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

  • Files reviewed: 6/6 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment thread src/spec-configuration/httpOCIRegistry.ts Outdated
Comment thread src/spec-configuration/httpOCIRegistry.ts
Kaniska (Kaniska244) and others added 2 commits August 11, 2026 17:29
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
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.

2 participants