Skip to content

fix(http): preserve redirect provenance in HTTP transfer cache - #70946

Open
Adyej999 wants to merge 1 commit into
angular:mainfrom
Adyej999:fix/http-transfer-cache-redirect-provenance
Open

Adyej999 wants to merge 1 commit into
angular:mainfrom
Adyej999:fix/http-transfer-cache-redirect-provenance

Conversation

@Adyej999

@Adyej999 Adyej999 commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Fixes 70947

Google OSS VRP referred the underlying report to the Angular maintainers for public issue and pull-request coordination.

Google OSS VRP report: 559659097

Problem

HttpTransferCache serializes a reduced HttpResponse into TransferState.

Before this change, replay did not preserve Fetch redirect provenance:

  • HttpResponse.redirected was lost.
  • HttpResponse.url was reconstructed from the TransferCache request URL instead of the final response URL.

This can cause redirect-aware application interceptors to observe different response semantics during SSR and browser hydration.

For example:

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

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

The linked issue contains the complete security scenario and demonstrated impact.

Change

This patch:

  • keeps the request URL used for TransferCache lookup semantics;
  • stores the final response URL separately when it differs from the request URL;
  • preserves HttpResponse.redirected when provided by the backend;
  • restores the final response URL and redirect flag into the replayed HttpResponse;
  • applies the existing HTTP_TRANSFER_CACHE_ORIGIN_MAP mapping to the final response URL when applicable.

The additional serialized properties are optional, so existing TransferState entries without them continue to use the current request-URL fallback behavior.

Regression test

The regression test:

  1. performs an SSR-side request for a trusted URL;
  2. returns an HttpResponse with redirected: true and a different final origin;
  3. allows TransferCache to serialize it;
  4. restores the request in browser mode;
  5. verifies the request is fulfilled from TransferCache;
  6. verifies that the body, final response URL, and redirect flag describe the same response observed during SSR.

Against the unmodified implementation on the tested main base:

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 this patch:

411 specs, 0 failures

Test target:

//packages/common/http/test:test

ng-dev format changed upstream/main --check and git diff --check also pass.

Scope

This PR addresses the response-provenance loss that causes the demonstrated SSR/browser security-policy mismatch.

It intentionally does not change interceptor ordering or the point at which TransferCache commits a response. Such a change would have broader behavioral implications and can be considered separately if desired.

@pullapprove
pullapprove Bot requested a review from crisbeto September 25, 2026 15:41
@angular-robot angular-robot Bot added the area: common Issues related to APIs in the @angular/common package label Sep 25, 2026
@ngbot ngbot Bot added this to the Backlog milestone Sep 25, 2026
@SkyZeroZx

SkyZeroZx commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

This a duplicate of #69531
And see #69531 (comment).

As note I don't think this can be considered a security more than harderning.

Also to reject a redirect you can use redirect: 'error'

@Adyej999
Adyej999 force-pushed the fix/http-transfer-cache-redirect-provenance branch from a74012c to f9adbba Compare September 25, 2026 16:40
@Adyej999

Copy link
Copy Markdown
Contributor Author

Thanks for pointing me to #69531. I reviewed its TransferCache changes and agree that there is overlap around HttpResponse.redirected.

There is one behavior in this PR that #69531 does not appear to cover, though: preserving the final response URL separately from the request URL.

#69531 stores redirected and the Fetch response type, but the reconstructed HttpResponse.url still comes from REQ_URL.

The regression here specifically exercises a case where those URLs differ:

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

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

Current TransferCache replay produces:

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

With the redirected preservation from #69531 alone, the replay would still be approximately:

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

so an interceptor which intentionally follows redirects but validates the final origin would still lose the destination needed for that decision.

Angular's interceptor documentation also describes using event.redirected together with event.url for redirect handling, including security checks.

Regarding redirect: 'error': I agree that this is an effective mitigation when the application's intended policy is to reject every redirect. The case here is different: an application allows redirects but applies policy to the final destination, for example allowing an expected redirect while rejecting an unexpected cross-origin one.

The security-relevant framework behavior reported through OSS VRP is the change in the result of the same application policy across the SSR/hydration boundary:

actual SSR response:
redirected = true
final URL = unexpected origin
→ interceptor rejects

TransferCache replay:
redirect provenance/final destination is lost
→ same interceptor accepts

The report also contains an end-to-end credential-disclosure consequence under additional application prerequisites. I'm not claiming that Angular applications generally leak credentials; the underlying framework behavior is the provenance change above.

I also updated this PR to take the TransferState payload concern raised on #69531 into account. Redirect provenance is now serialized only when meaningful:

...(responseUrl !== null && responseUrl !== requestUrl
  ? {[RESPONSE_URL]: responseUrl}
  : {}),
...(redirected === true ? {[REDIRECTED]: true} : {}),

Therefore, for a non-redirected response whose final URL equals the request URL, neither additional field is serialized. There is now a regression test for that behavior as well.

@JeanMeche
JeanMeche requested review from alan-agius4 and removed request for crisbeto September 25, 2026 19:08
@pullapprove
pullapprove Bot requested a review from crisbeto September 25, 2026 19:08

@alan-agius4 alan-agius4 left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for working on this.

Please note that this change is considered hardening rather than a security issue.

Please update the commit header from fix(common) to fix(http) per the Angular commit guidelines. I also left a few additional comments inline next to the code.

Comment thread packages/common/http/src/transfer_cache.ts
Comment thread packages/common/http/test/transfer_cache_spec.ts Outdated
Comment thread packages/common/http/test/transfer_cache_spec.ts Outdated
@alan-agius4 alan-agius4 added the target: patch This PR is targeted for the next patch release label Sep 28, 2026
@Adyej999
Adyej999 force-pushed the fix/http-transfer-cache-redirect-provenance branch from f9adbba to 7f0f410 Compare September 28, 2026 15:10
@angular-robot angular-robot Bot added the area: common/http Issues related to HTTP and HTTP Client label Sep 28, 2026
@Adyej999 Adyej999 changed the title fix(common): preserve redirect provenance in HTTP transfer cache fix(http): preserve redirect provenance in HTTP transfer cache Sep 28, 2026
@Adyej999

Copy link
Copy Markdown
Contributor Author

Thanks for the review. I've pushed an updated version of the PR and addressed the requested changes.

Changes made:

  • Updated the commit scope from fix(common) to fix(http).
  • Moved the redirect provenance tests inside describe('transferCacheInterceptorFn') so they use the existing runOnServer / runInterceptor helpers.
  • Added a regression test verifying that HTTP_TRANSFER_CACHE_ORIGIN_MAP correctly maps the final response URL when a redirect occurs.
  • Replaced the hasOwnProperty assertions with the requested Jasmine toBeUndefined() matchers.
  • Rebasing was also done against the latest main.

The focused //packages/common/http/test:test target passes, and the changed files pass formatting and git diff --check.

Regarding the classification as hardening rather than a security issue: understood. For my own understanding, could you clarify what makes this fall on the hardening side?

The reason I ask is that the report demonstrates a case where the same redirect-aware application policy can produce different results across SSR and hydration because the transferred response loses the final response URL / redirect provenance.

Under the additional application prerequisites described in the report, we were also able to demonstrate a conditional credential-disclosure chain: an attacker-controlled redirected response could be accepted from TransferCache during hydration, influence a runtime API base URL, and cause a later application auth interceptor to send a bearer token to the attacker-controlled origin. The same chain was blocked when the real redirect provenance was preserved.

I'm not suggesting that Angular directly leaks credentials in every application, since the token-disclosure step depends on application-specific behavior and the prerequisites described in the report. I'm mainly trying to understand which part of that demonstrated chain is considered outside Angular's security boundary, so I can apply the same criterion to future reports.

@alan-agius4
alan-agius4 requested review from JeanMeche and removed request for crisbeto September 29, 2026 08:07

@alan-agius4 alan-agius4 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM

@alan-agius4

Copy link
Copy Markdown
Contributor

Regarding the classification as hardening versus a security vulnerability:

In the context of the framework, a security vulnerability generally refers to a flaw where Angular directly enables an exploit or breaks an internal security boundary (for example, a sanitizer bypass, template injection leading to XSS, or an SSRF triggered by framework internals).

In this case:

  1. Fidelity vs. Security Boundary: TransferCache is an optimization mechanism that records HTTP responses on the server and replays them during client hydration to avoid duplicate requests. The issue was primarily a fidelity bug where reconstructed HttpResponse instances dropped metadata (redirected and final url) that was present on the server.
  2. Application-Level Preconditions: The potential for security impact (such as token leakage) relies on specific application design choices:
    • An SSR request fetching untrusted/redirectable resources.
    • An interceptor or business logic that chooses to validate destination origins after following redirects rather than rejecting unexpected redirects up front (e.g. redirect: 'error').
    • Downstream logic dynamically trusting the resulting endpoint for sensitive follow-up requests without independent validation.
  3. Defense-in-Depth: While preserving redirect provenance allows applications that implement client-side redirect policies to make consistent decisions across SSR and hydration, the framework itself is not vulnerable rather, preserving this metadata provides defense-in-depth and eliminates an unexpected discrepancy between server and client states.

Thanks again.

@alan-agius4 alan-agius4 added the action: review The PR is still awaiting reviews from at least one requested reviewer label Sep 29, 2026
@JeanMeche JeanMeche added the action: cleanup The PR is in need of cleanup, either due to needing a rebase or in response to comments from reviews label Sep 29, 2026
@JeanMeche

Copy link
Copy Markdown
Member

Can you please address the testing failure. (the internal test one is a flake).

Preserve the final response URL and redirected flag when HTTP responses are
serialized into TransferState and replayed on the client.

This ensures redirect-aware interceptors observe consistent response provenance
across SSR and client hydration.

Only serialize redirect provenance when it is meaningful: redirected is stored
only for actual redirects, and the final response URL is stored only when it
differs from the request URL. This avoids adding fields to ordinary
non-redirected TransferCache entries.
@Adyej999
Adyej999 force-pushed the fix/http-transfer-cache-redirect-provenance branch from 7f0f410 to b89ec22 Compare September 29, 2026 20:56
@Adyej999

Copy link
Copy Markdown
Contributor Author

Can you please address the testing failure. (the internal test one is a flake).

Thanks for pointing this out.

I addressed the public testing failure. The issue was that the two new TransferCache symbols, REDIRECTED and RESPONSE_URL, were not yet present in the hydration bundle golden-symbol file.

I updated packages/core/test/bundling/hydration/bundle.golden_symbols.json and rebased again onto the latest main.

I verified the final commit locally:

  • //packages/core/test/bundling/hydration:symbol_test passes.
  • //packages/common/http/test:test passes.
  • Formatting passes.
  • git diff --check is clean.

The updated commit has now been pushed.

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

Labels

action: cleanup The PR is in need of cleanup, either due to needing a rebase or in response to comments from reviews action: review The PR is still awaiting reviews from at least one requested reviewer area: common/http Issues related to HTTP and HTTP Client area: common Issues related to APIs in the @angular/common package target: patch This PR is targeted for the next patch release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

4 participants