mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
security(sse): validate clientId shape and cap subscribe payload
Constrains the per-client SSE identifier introduced in #86 to `[A-Za-z0-9_-]{8,64}` at both ingress points (`GET /api/events` query and `POST /api/events/subscribe` body). Without this, an authenticated attacker could: - Send a victim's clientId to silently evict their tab from sseClients (DoS — socket stays open, broadcasts stop). - Mutate any clientId's session filter, blackholing that tab's terminal stream. - Grow sseClientsById without bound via long IDs. Also caps the subscribe payload to 64 session entries of ≤128 chars each, since the previous handler accepted arbitrary-length arrays. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+11
-3
@@ -119,6 +119,11 @@ import {
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
// Bounded, predictable shape for SSE client identifiers: alphanumerics, `_`, `-`.
|
||||
// Length range covers crypto.randomUUID() (36 chars) plus any short stable IDs,
|
||||
// while capping growth of `sseClientsById` and blocking pathological inputs.
|
||||
const SSE_CLIENT_ID_RE = /^[A-Za-z0-9_-]{8,64}$/;
|
||||
|
||||
function escapeHtmlText(value: string): string {
|
||||
return value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>');
|
||||
}
|
||||
@@ -594,7 +599,8 @@ export class WebServer extends EventEmitter {
|
||||
sessionFilter = new Set(ids);
|
||||
}
|
||||
}
|
||||
const clientId = typeof query.clientId === 'string' && query.clientId ? query.clientId : undefined;
|
||||
const clientId =
|
||||
typeof query.clientId === 'string' && SSE_CLIENT_ID_RE.test(query.clientId) ? query.clientId : undefined;
|
||||
|
||||
reply.raw.writeHead(200, {
|
||||
'Content-Type': 'text/event-stream',
|
||||
@@ -626,11 +632,13 @@ export class WebServer extends EventEmitter {
|
||||
// Empty/null sessions array = remove filter (receive all session:terminal events).
|
||||
this.app.post('/api/events/subscribe', (req, reply) => {
|
||||
const body = (req.body || {}) as { clientId?: string; sessions?: string[] | null };
|
||||
if (!body.clientId || typeof body.clientId !== 'string') {
|
||||
if (typeof body.clientId !== 'string' || !SSE_CLIENT_ID_RE.test(body.clientId)) {
|
||||
reply.code(400).send({ error: 'clientId required' });
|
||||
return;
|
||||
}
|
||||
const sessions = Array.isArray(body.sessions) ? body.sessions.filter((s) => typeof s === 'string') : null;
|
||||
const sessions = Array.isArray(body.sessions)
|
||||
? body.sessions.filter((s) => typeof s === 'string' && s.length > 0 && s.length <= 128).slice(0, 64)
|
||||
: null;
|
||||
const updated = this.sse.updateClientFilter(body.clientId, sessions);
|
||||
reply.code(updated ? 204 : 404).send();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user