Repository navigation
Conversation
…x` header The `X-Forwarded-Prefix` header is validated using its first comma-separated value, like the other `X-Forwarded-*` headers. However, redirects used the entire header value. With a trusted header, any value after the first one was added to the `Location` header without being validated. The first value is now used wherever the header is read, which matches how `X-Forwarded-Host`, `X-Forwarded-Proto` and `X-Forwarded-Port` are handled.
Redirects created on the server can contain data from the request. Route redirects keep the path and the query string, redirects done by the router while rendering use its final URL, and the `X-Forwarded-Prefix` header is prepended. The router also encodes some characters again, so the `Location` header can be several times longer than the request URL. Reverse proxies read the response headers into a buffer of limited size, 4 KB by default in NGINX. When the headers do not fit, the proxy returns a 502 and, with passive health checks, counts it against the server. A few requests could take every server out of rotation. `AngularAppEngine` and `AngularNodeAppEngine` now return a 400 when a redirect location is longer than 2048 characters. The limit can be changed with the new `maxRedirectLocationLength` option. Redirects created while prerendering are not limited. Fixes angular#34365
There was a problem hiding this comment.
Code Review
This pull request introduces a configurable 'maxRedirectLocationLength' option to the Angular SSR engine to prevent issues with reverse proxy header buffer limits. It adds validation logic to 'createRedirectResponse' to return a 400 Bad Request when the redirect location exceeds the defined limit. The reviewer suggested an improvement to ensure that original headers (such as security or CORS headers) are preserved when returning the 400 error response, rather than discarding them.
| if (maxLocationLength !== undefined && location.length > maxLocationLength) { | ||
| const errorMessage = | ||
| `Redirect location is ${location.length} characters long, which exceeds ` + | ||
| `the maximum allowed length of ${maxLocationLength} characters.`; | ||
|
|
||
| // eslint-disable-next-line no-console | ||
| console.error( | ||
| `ERROR: Bad Request.\n${errorMessage}\n` + | ||
| 'The limit can be changed with the "maxRedirectLocationLength" option of "AngularAppEngine" or "AngularNodeAppEngine".', | ||
| ); | ||
|
|
||
| return new Response(errorMessage, { | ||
| status: 400, | ||
| statusText: 'Bad Request', | ||
| headers: { 'Content-Type': 'text/plain' }, | ||
| }); | ||
| } | ||
|
|
||
| const resHeaders = headers instanceof Headers ? headers : new Headers(headers); |
There was a problem hiding this comment.
When returning a 400 Bad Request response due to exceeding the maximum redirect location length, the original headers (such as CORS headers, security headers, etc.) passed to createRedirectResponse are completely discarded. To ensure these headers are preserved in the error response, we should initialize resHeaders first, remove any potential location header, set the Content-Type to text/plain, and pass resHeaders to the Response constructor.
const resHeaders = headers instanceof Headers ? headers : new Headers(headers);
if (maxLocationLength !== undefined && location.length > maxLocationLength) {
const errorMessage =
`Redirect location is ${location.length} characters long, which exceeds ` +
`the maximum allowed length of ${maxLocationLength} characters.`;
// eslint-disable-next-line no-console
console.error(
`ERROR: Bad Request.\\n${errorMessage}\\n` +
'The limit can be changed with the "maxRedirectLocationLength" option of "AngularAppEngine" or "AngularNodeAppEngine".',
);
resHeaders.delete('location');
resHeaders.set('Content-Type', 'text/plain');
return new Response(errorMessage, {
status: 400,
statusText: 'Bad Request',
headers: resHeaders,
});
}There was a problem hiding this comment.
I think we should keep it as is. The headers passed here are the route ones (like Cache-Control), so if we keep them in the 400, a request that hits the limit could get cached by a CDN and served to other users.
Headers like CSP or CORS from helmet() or cors() are not lost, since the middleware sets them on the Node response ( I checked it and the 400 still has all the helmet() headers ).
Also, this is the same as the 400 we return for invalid hosts, and there is a test that checks that Cache-Control is not kept.
|
We don't think
Could you please open a separate PR for the |
fix(@angular/ssr): use only the first value of the X-Forwarded-Prefix header
The
X-Forwarded-Prefixheader is validated using its firstcomma-separated value, like the other
X-Forwarded-*headers. However,redirects used the entire header value. With a trusted header, any
value after the first one was added to the
Locationheader withoutbeing validated.
The first value is now used wherever the header is read, which matches
how
X-Forwarded-Host,X-Forwarded-ProtoandX-Forwarded-Portarehandled.
fix(@angular/ssr): limit the length of redirect locations
Redirects created on the server can contain data from the request.
Route redirects keep the path and the query string, redirects done by
the router while rendering use its final URL, and the
X-Forwarded-Prefixheader is prepended. The router also encodes somecharacters again, so the
Locationheader can be several times longerthan the request URL.
Reverse proxies read the response headers into a buffer of limited
size, 4 KB by default in NGINX. When the headers do not fit, the proxy
returns a 502 and, with passive health checks, counts it against the
server. A few requests could take every server out of rotation.
AngularAppEngineandAngularNodeAppEnginenow return a 400 when aredirect location is longer than 2048 characters. The limit can be
changed with the new
maxRedirectLocationLengthoption. Redirectscreated while prerendering are not limited.
Fixes #34365