Filter the response headers in the proxy backend
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/plugin-proxy-backend': patch
|
||||
---
|
||||
|
||||
Filter the headers that are sent from the proxied-targed back to the frontend to not forwarded unwanted authentication or
|
||||
monitoring contexts from other origins (like `Set-Cookie` with e.g. a google analytics context). The implementation reuses
|
||||
the `allowedHeaders` configuration that now controls both directions `frontend->target` and `target->frontend`.
|
||||
@@ -78,7 +78,8 @@ There are also additional settings:
|
||||
|
||||
- `allowedMethods`: Limit the forwarded HTTP methods. For example
|
||||
`allowedMethods: ['GET']` enforces read-only access.
|
||||
- `allowedHeaders`: A list of headers that should be forwarded to the target.
|
||||
- `allowedHeaders`: A list of headers that should be forwarded to and received
|
||||
from the target.
|
||||
|
||||
By default, the proxy will only forward safe HTTP request headers to the target.
|
||||
Those are based on the headers that are considered safe for CORS and includes
|
||||
@@ -88,3 +89,6 @@ set by the proxy. If the proxy should forward other headers like
|
||||
example `allowedHeaders: ['Authorization']`. This should help to not
|
||||
accidentally forward confidential headers (`cookie`, `X-Auth-Request-User`) to
|
||||
third-parties.
|
||||
|
||||
The same logic applies to headers that are sent from the target back to the
|
||||
frontend.
|
||||
|
||||
@@ -198,4 +198,74 @@ describe('buildMiddleware', () => {
|
||||
'X-Auth-Request-User',
|
||||
);
|
||||
});
|
||||
|
||||
it('responds default headers', async () => {
|
||||
buildMiddleware('/api/', logger, 'test', {
|
||||
target: 'http://mocked',
|
||||
});
|
||||
|
||||
expect(createProxyMiddleware).toHaveBeenCalledTimes(1);
|
||||
|
||||
const config = mockCreateProxyMiddleware.mock
|
||||
.calls[0][1] as ProxyMiddlewareConfig;
|
||||
|
||||
const testClientResponse = {
|
||||
headers: {
|
||||
'cache-control': 'value',
|
||||
'content-language': 'value',
|
||||
'content-length': 'value',
|
||||
'content-type': 'value',
|
||||
expires: 'value',
|
||||
'last-modified': 'value',
|
||||
pragma: 'value',
|
||||
'set-cookie': ['value'],
|
||||
},
|
||||
} as Partial<http.IncomingMessage>;
|
||||
|
||||
expect(config).toBeDefined();
|
||||
expect(config.onProxyReq).toBeDefined();
|
||||
|
||||
config.onProxyRes!(
|
||||
testClientResponse as http.IncomingMessage,
|
||||
{} as http.IncomingMessage,
|
||||
{} as http.ServerResponse,
|
||||
);
|
||||
|
||||
expect(Object.keys(testClientResponse.headers!)).toEqual([
|
||||
'cache-control',
|
||||
'content-language',
|
||||
'content-length',
|
||||
'content-type',
|
||||
'expires',
|
||||
'last-modified',
|
||||
'pragma',
|
||||
]);
|
||||
});
|
||||
|
||||
it('responds configured headers', async () => {
|
||||
buildMiddleware('/api/', logger, 'test', {
|
||||
target: 'http://mocked',
|
||||
allowedHeaders: ['set-cookie'],
|
||||
});
|
||||
|
||||
expect(createProxyMiddleware).toHaveBeenCalledTimes(1);
|
||||
|
||||
const config = mockCreateProxyMiddleware.mock
|
||||
.calls[0][1] as ProxyMiddlewareConfig;
|
||||
|
||||
const testClientResponse = {
|
||||
headers: {
|
||||
'set-cookie': [],
|
||||
'x-auth-request-user': 'asd',
|
||||
},
|
||||
} as Partial<http.IncomingMessage>;
|
||||
|
||||
config.onProxyRes!(
|
||||
testClientResponse as http.IncomingMessage,
|
||||
{} as http.IncomingMessage,
|
||||
{} as http.ServerResponse,
|
||||
);
|
||||
|
||||
expect(Object.keys(testClientResponse.headers!)).toEqual(['set-cookie']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -90,8 +90,8 @@ export function buildMiddleware(
|
||||
return fullConfig?.allowedMethods?.includes(req.method!) ?? true;
|
||||
};
|
||||
|
||||
// Only forward the allowed HTTP headers to not forward unwanted secret headers
|
||||
const headerAllowList = new Set<string>(
|
||||
// Only return the allowed HTTP headers to not forward unwanted secret headers
|
||||
const requestHeaderAllowList = new Set<string>(
|
||||
[
|
||||
// allow all safe headers
|
||||
...safeForwardHeaders,
|
||||
@@ -104,16 +104,39 @@ export function buildMiddleware(
|
||||
].map(h => h.toLocaleLowerCase()),
|
||||
);
|
||||
|
||||
// only forward the allowed headers in client->backend
|
||||
fullConfig.onProxyReq = (proxyReq: http.ClientRequest) => {
|
||||
const headerNames = proxyReq.getHeaderNames();
|
||||
|
||||
headerNames.forEach(h => {
|
||||
if (!headerAllowList.has(h.toLocaleLowerCase())) {
|
||||
if (!requestHeaderAllowList.has(h.toLocaleLowerCase())) {
|
||||
proxyReq.removeHeader(h);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Only forward the allowed HTTP headers to not forward unwanted secret headers
|
||||
const responseHeaderAllowList = new Set<string>(
|
||||
[
|
||||
// allow all safe headers
|
||||
...safeForwardHeaders,
|
||||
|
||||
// allow all configured headers
|
||||
...(fullConfig.allowedHeaders || []),
|
||||
].map(h => h.toLocaleLowerCase()),
|
||||
);
|
||||
|
||||
// only forward the allowed headers in backend->client
|
||||
fullConfig.onProxyRes = (proxyRes: http.IncomingMessage) => {
|
||||
const headerNames = Object.keys(proxyRes.headers);
|
||||
|
||||
headerNames.forEach(h => {
|
||||
if (!responseHeaderAllowList.has(h.toLocaleLowerCase())) {
|
||||
delete proxyRes.headers[h];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return createProxyMiddleware(filter, fullConfig);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user