mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-12 20:15:40 +00:00
fix(web): harden localhost guard against DNS rebinding (#1357)
* fix(web): harden localhost guard against DNS rebinding * style: apply prettier formatting
This commit is contained in:
1 parent
0bd2e577b2
commit
21764d5b90
2 files changed
+69
-7
No files matched your search
@@ -246,10 +246,36 @@ export function requireLocalAccessWhenAuthDisabled(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLoopbackRemoteAddress(req.socket.remoteAddress)) {
|
if (!isLoopbackRemoteAddress(req.socket.remoteAddress)) {
|
||||||
return true;
|
res.status(403).json({ error });
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(403).json({ error });
|
const host = parseHostHeader(getSingleHeader(req.headers.host));
|
||||||
return false;
|
if (!host || !isLoopbackHostname(host.hostname)) {
|
||||||
|
res.status(403).json({ error });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const originHeader = getSingleHeader(req.headers.origin);
|
||||||
|
if (originHeader) {
|
||||||
|
let origin: URL;
|
||||||
|
try {
|
||||||
|
origin = new URL(originHeader);
|
||||||
|
} catch {
|
||||||
|
res.status(403).json({ error });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSameHost = origin.host.toLowerCase() === host.host.toLowerCase();
|
||||||
|
const isLoopbackAlias =
|
||||||
|
isHttpOrigin(origin) && isLoopbackHostname(origin.hostname) && origin.port === host.port;
|
||||||
|
|
||||||
|
if (!isSameHost && !isLoopbackAlias) {
|
||||||
|
res.status(403).json({ error });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
@@ -206,9 +206,8 @@ describe('GET /api/codex/profiles', () => {
|
|||||||
// 127.0.0.1 and the test client connects to 127.0.0.1, the built-in fetch
|
// 127.0.0.1 and the test client connects to 127.0.0.1, the built-in fetch
|
||||||
// will always be loopback. We test the guard directly via a separate
|
// will always be loopback. We test the guard directly via a separate
|
||||||
// Express app that injects a non-loopback remote address.
|
// Express app that injects a non-loopback remote address.
|
||||||
const { requireLocalAccessWhenAuthDisabled } = await import(
|
const { requireLocalAccessWhenAuthDisabled } =
|
||||||
'../../../src/web-server/middleware/auth-middleware'
|
await import('../../../src/web-server/middleware/auth-middleware');
|
||||||
);
|
|
||||||
const { isDashboardAuthEnabled } = await import('../../../src/config/config-loader-facade');
|
const { isDashboardAuthEnabled } = await import('../../../src/config/config-loader-facade');
|
||||||
|
|
||||||
if (!isDashboardAuthEnabled()) {
|
if (!isDashboardAuthEnabled()) {
|
||||||
@@ -245,6 +244,43 @@ describe('GET /api/codex/profiles', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('returns 403 for loopback remote addresses when host/origin indicate non-local origin', async () => {
|
||||||
|
const { requireLocalAccessWhenAuthDisabled } =
|
||||||
|
await import('../../../src/web-server/middleware/auth-middleware');
|
||||||
|
const { isDashboardAuthEnabled } = await import('../../../src/config/config-loader-facade');
|
||||||
|
|
||||||
|
if (!isDashboardAuthEnabled()) {
|
||||||
|
let guardResult: boolean | undefined;
|
||||||
|
|
||||||
|
const testApp = express();
|
||||||
|
testApp.get('/test', (req, res) => {
|
||||||
|
Object.defineProperty(req, 'socket', {
|
||||||
|
value: { remoteAddress: '127.0.0.1' },
|
||||||
|
writable: true,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
req.headers.host = 'attacker.example.test';
|
||||||
|
req.headers.origin = 'http://attacker.example.test';
|
||||||
|
|
||||||
|
guardResult = requireLocalAccessWhenAuthDisabled(req, res, 'localhost only');
|
||||||
|
if (guardResult) {
|
||||||
|
res.json({ ok: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const testServer = await new Promise<http.Server>((resolve) => {
|
||||||
|
const s = testApp.listen(0, '127.0.0.1', () => resolve(s));
|
||||||
|
});
|
||||||
|
const testPort = (testServer.address() as { port: number }).port;
|
||||||
|
|
||||||
|
const res = await fetch(`http://127.0.0.1:${testPort}/test`);
|
||||||
|
await new Promise<void>((resolve) => testServer.close(() => resolve()));
|
||||||
|
|
||||||
|
expect(res.status).toBe(403);
|
||||||
|
expect(guardResult).toBe(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('response body contains no token substrings for a valid profile', async () => {
|
it('response body contains no token substrings for a valid profile', async () => {
|
||||||
const instancesDir = path.join(ccsDir, 'codex-instances');
|
const instancesDir = path.join(ccsDir, 'codex-instances');
|
||||||
const workDir = path.join(instancesDir, 'work');
|
const workDir = path.join(instancesDir, 'work');
|
||||||
|
|||||||
Reference in new issue
Block a user