gateway-backend: add loop detection

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2025-11-18 11:58:56 +01:00
parent 766215437e
commit 229f63ef2e
3 changed files with 44 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-gateway-backend': minor
---
Added hop count tracking to prevent proxy loops. The gateway now tracks the number of proxy hops using the `backstage-gateway-hops` header and rejects requests that exceed 3 hops with a 508 Loop Detected error.
@@ -161,6 +161,21 @@ describe('gateway', () => {
expect(data).toEqual({ bar: true });
});
it('should detect looped requests', async () => {
const response = await fetch(
'http://localhost:7777/api/nonexistent-plugin/foo',
);
expect(response.status).toBe(508);
const data = await response.json();
expect(data).toEqual({
error: {
name: 'LoopDetectedError',
message: 'Maximum proxy hop count exceeded (3)',
},
});
});
it('should close the response for sse connections', async () => {
const eventSource = new EventSource(
'http://localhost:7777/api/external-plugin/endpoint-sse',
+24
View File
@@ -23,9 +23,13 @@ import { createProxyMiddleware } from 'http-proxy-middleware';
import { context } from '@opentelemetry/api';
import { getRPCMetadata } from '@opentelemetry/core';
const MAX_HOPS = 3;
const HOPS_HEADER = 'backstage-gateway-hops';
export async function createRouter({
discovery,
instanceMeta,
logger,
}: {
discovery: DiscoveryService;
instanceMeta: RootInstanceMetadataService;
@@ -41,6 +45,12 @@ export async function createRouter({
return discovery.getBaseUrl(pluginId);
},
on: {
proxyReq(proxyReq, req: Request<{ pluginId: string }>) {
const currentHops =
parseInt(req.headers[HOPS_HEADER] as string, 10) || 0;
proxyReq.setHeader(HOPS_HEADER, currentHops + 1);
},
proxyRes(proxyRes, _req, res) {
// https://github.com/chimurai/http-proxy-middleware/discussions/765
proxyRes.on('close', () => {
@@ -62,6 +72,20 @@ export async function createRouter({
return;
}
const currentHops = parseInt(req.headers[HOPS_HEADER] as string, 10) || 0;
if (currentHops >= MAX_HOPS) {
logger.warn(
`Proxy loop detected for plugin '${req.params.pluginId}': request exceeded maximum hop count (${currentHops})`,
);
res.status(508).json({
error: {
name: 'LoopDetectedError',
message: `Maximum proxy hop count exceeded (${currentHops})`,
},
});
return;
}
const rpcMetadata = getRPCMetadata(context.active());
if (rpcMetadata) {
rpcMetadata.route = req.baseUrl;