test(e2e): add persona e2e suites

Power, careless, malicious, fuzzing, cross-abuse, operator-session and concurrency personas exercising the whole application with model-checked database state.
This commit is contained in:
bryanpedini committed 2026-10-06 10:52:25 +02:00
1 parent 581a672fcf
commit a139f6793d
7 files changed
+2983

No files matched your search

@@ -0,0 +1,935 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// Copyright (C) 2026 Bryan Joshua Pedini
/**
* PLAN §12 persona "Malicious user": attempts to bypass authentication, obtain information,
* corrupt state or cause unsafe behaviour through every interface that exists (HTTP via
* Playwright / curl / raw socket, the CLI, the shared SQLite file, the Docker socket).
*
* Every test asserts: the exact status/exit outcome; no token secret (or 12-char fragment of
* one) in any response or log; no internal detail in HTTP bodies; exact audit rows; structurally
* valid database (assertDbInvariants) and, where the attack must not change anything, a
* byte-identical logical state (stateDigest).
*/
import { DatabaseSync } from 'node:sqlite';
import fs from 'node:fs';
import {
test,
expect,
assertDbInvariants,
expectCliFailure,
expectCliSuccess,
expectContainersResponse,
expectErrorResponse,
expectNoInternalDetails,
fileDigest,
goQuote,
mapLimit,
msToNanos,
parseAuditOutput,
parseLogs,
parseTokenList,
pollUntil,
readOperations,
readSchema,
readToken,
rfc3339ToNanos,
shellQuote,
tokenId,
USAGE_TEXT,
type HttpClient,
type RawResult,
type ServerHandle,
} from '../../harness';
import {
ENDPOINT,
SQL_PAYLOADS,
StateModel,
TOKEN_CHARS,
auditArgsOutcome,
authOf,
expectedAuditStdout,
expectNoTokenFragments,
nearMatches,
onlyResponse,
routeOf,
stateDigest,
} from '../../harness/personas';
/** Raw request bytes (UTF-8): request line, Host, given header lines, optional Connection: close. */
function req(lines: string[], { target = ENDPOINT, method = 'GET', version = 'HTTP/1.1', close = true, body }: { target?: string; method?: string; version?: string; close?: boolean; body?: string } = {}): Buffer {
const all = [`${method} ${target} ${version}`, 'Host: localhost', ...lines];
if (body !== undefined) all.push(`Content-Length: ${Buffer.byteLength(body)}`);
if (close) all.push('Connection: close');
return Buffer.from(all.join('\r\n') + '\r\n\r\n' + (body ?? ''), 'utf8');
}
function raw(http: HttpClient, s: ServerHandle, data: Buffer | string, extra: { expectResponses?: number; timeoutMs?: number } = {}): Promise<RawResult> {
return http.raw({ host: s.connectHost, port: s.port, data: typeof data === 'string' ? Buffer.from(data, 'utf8') : data, timeoutMs: 20_000, ...extra });
}
test('token enumeration: 1500 random guesses, near-matches, case/whitespace/confusable mutations and inactive tokens — only the documented OWS trim authenticates, responses are indistinguishable', async ({ cli, startServer, http, secrets, rng }) => {
test.setTimeout(120_000);
const model = new StateModel();
for (let i = 0; i < 5; i++) model.add(await cli.tokenNew(`victim ${i}`), `victim ${i}`);
for (let i = 0; i < 4; i++) {
const t = model.add(await cli.tokenNew(`revoked ${i}`), `revoked ${i}`).token;
await cli.tokenDelete(t);
model.deactivate(t);
}
const server = await startServer({ logLevel: 'DEBUG' });
server.fakeDocker!.many(2);
const lookup = (t: string) => model.lookup(t);
const expectedClasses: string[] = [];
await test.step('1500 random 43-character guesses, 24 concurrent (Playwright)', async () => {
const guesses = Array.from({ length: 1500 }, () => rng.string(43, TOKEN_CHARS));
const res = await mapLimit(guesses, 24, (g) => http.get(server.url(), { token: g }));
for (const r of res) expectErrorResponse(r, 401);
expectedClasses.push(...guesses.map(() => 'unknown_token'));
});
await test.step('near-match mutations of every active and inactive token (raw socket, UTF-8 bytes)', async () => {
const cases: Array<{ kind: string; header: string }> = [];
for (const t of model.all()) {
for (const m of nearMatches(t, rng, 30)) if (!/[\r\n]/.test(m.value)) cases.push({ kind: m.kind, header: `Bearer ${m.value}` });
for (const v of [`bearer ${t}`, `BEARER ${t}`, ` Bearer ${t}`, `Bearer ${t}\t`, `Bearer ${t} `, `Bearer\u{a0}${t}`, `Bearer ${t}\u{200b}`, `B\u{435}arer ${t}`]) cases.push({ kind: 'scheme/whitespace', header: v });
}
let oks = 0;
await mapLimit(cases, 16, async (c) => {
const exp = authOf([c.header], lookup);
const r = onlyResponse(await raw(http, server, req([`Authorization: ${c.header}`])), c.kind);
if (exp.cls === 'ok') {
// documented: HTTP itself trims leading/trailing whitespace of the header value (RFC 9110)
expectContainersResponse(r);
model.audit(exp.token!);
oks++;
} else expectErrorResponse(r, 401);
expectedClasses.push(exp.cls);
});
expect(oks, 'only OWS-trimmed exact tokens of active tokens authenticated').toBe(cases.filter((c) => authOf([c.header], lookup).cls === 'ok').length);
expect(cases.length).toBeGreaterThan(300);
});
await test.step('inactive tokens hammered: 4 × 25 concurrent requests → 401 every time', async () => {
const attempts = model.inactive().flatMap((t) => Array.from({ length: 25 }, () => t));
for (const r of await mapLimit(attempts, 20, (t) => http.get(server.url(), { token: t }))) expectErrorResponse(r, 401);
expectedClasses.push(...attempts.map(() => 'inactive_token'));
});
await test.step('unknown, inactive, malformed and missing credentials are indistinguishable to the client', async () => {
const shape = (r: RawResult) => {
const res = onlyResponse(r, 'shape');
return { status: res.status, headers: res.headers.filter(([k]) => k.toLowerCase() !== 'date'), body: res.bodyText };
};
const variants = [
req([`Authorization: Bearer ${rng.string(43, TOKEN_CHARS)}`]),
req([`Authorization: Bearer ${model.inactive()[0]}`]),
req(['Authorization: Basic Zm9vOmJhcg==']),
req([]),
req([`Authorization: Bearer ${model.active()[0]}`, `Authorization: Bearer ${model.active()[0]}`]),
];
const shapes = [];
for (const v of variants) shapes.push(shape(await raw(http, server, v)));
for (const s of shapes.slice(1)) expect(s).toEqual(shapes[0]);
expectedClasses.push('unknown_token', 'inactive_token', 'bad_scheme', 'missing_header', 'multiple_headers');
});
// the legitimate users are unaffected
for (const t of model.active()) {
expect(expectContainersResponse(await http.get(server.url(), { token: t }))).toHaveLength(2);
model.audit(t);
expectedClasses.push('ok');
}
expect((await server.stop()).exitCode).toBe(0);
const logs = server.logs();
expect(logs.filter({ msg: 'authentication result' }).map((r) => r.attrs.result).sort(), 'every attempt was classified as the oracle predicts').toEqual([...expectedClasses].sort());
expect(logs.count({ level: ['WARN', 'ERROR'] })).toBe(0);
expectNoTokenFragments(server.stderr, secrets, 'server log');
model.verifyDb(server.dbPath);
});
interface InjectionCase {
name: string;
data: Buffer;
/** Expected status of each response, in order; `go` marks responses produced by Go (not the app). */
responses: Array<{ status: number; go?: boolean }>;
/** Tokens that must get one audit row each. */
audits: string[];
}
test('header injection, smuggling, duplicate/conflicting/oversized headers and malformed HTTP syntax (raw socket)', async ({ cli, startServer, http, secrets }) => {
test.setTimeout(90_000);
const T = await cli.tokenNew('valid');
const U = await cli.tokenNew('other');
const I = await cli.tokenNew('revoked');
await cli.tokenDelete(I);
const server = await startServer({ logLevel: 'DEBUG' });
server.fakeDocker!.many(1);
const ok = { status: 200 };
const c401 = { status: 401 };
const go400 = { status: 400, go: true };
const cases: InjectionCase[] = [
{ name: 'CR inside a value (attempted header split) → Go 400', data: req([`Authorization: Bearer junk\rAuthorization: Bearer ${T}`]), responses: [go400], audits: [] },
{ name: 'bare LF inside a value becomes a second Authorization header → 401 multiple', data: req([`Authorization: Bearer junk\nAuthorization: Bearer ${T}`]), responses: [c401], audits: [] },
{ name: 'obs-fold hides "Authorization:" inside X-A → 401 missing', data: req(['X-A: x', ` Authorization: Bearer ${T}`]), responses: [c401], audits: [] },
{ name: 'NUL inside the Authorization value → Go 400', data: req([`Authorization: Bearer ${T}\x00`]), responses: [go400], audits: [] },
{ name: 'NUL inside a header name → Go 400', data: req([`X-\x00: 1`, `Authorization: Bearer ${T}`]), responses: [go400], audits: [] },
{ name: 'space before the colon → Go 400', data: req([`Authorization : Bearer ${T}`]), responses: [go400], audits: [] },
{ name: 'valid + revoked under different name case → 401', data: req([`authorization: Bearer ${T}`, `AUTHORIZATION: Bearer ${I}`]), responses: [c401], audits: [] },
{ name: 'two different valid tokens → 401', data: req([`Authorization: Bearer ${T}`, `Authorization: Bearer ${U}`]), responses: [c401], audits: [] },
{ name: 'valid then empty Authorization → 401', data: req([`Authorization: Bearer ${T}`, 'Authorization:']), responses: [c401], audits: [] },
{ name: 'Authorization only in a chunked trailer → 401 missing', data: Buffer.from(`GET ${ENDPOINT} HTTP/1.1\r\nHost: x\r\nTransfer-Encoding: chunked\r\n\r\n1\r\na\r\n0\r\nAuthorization: Bearer ${T}\r\n\r\n`), responses: [c401], audits: [] },
{ name: 'Authorization smuggled in a GET body → 401 missing', data: req([], { body: `Authorization: Bearer ${T}\r\n`, close: false }), responses: [c401], audits: [] },
{ name: 'pipelined: valid, no auth, revoked on one connection → auth never carries over', data: Buffer.concat([req([`Authorization: Bearer ${T}`], { close: false }), req([], { close: false }), req([`Authorization: Bearer ${I}`])]), responses: [ok, c401, c401], audits: [T] },
{
name: 'CL+TE request smuggling: the request after the chunk terminator is a separate, unauthenticated request',
data: Buffer.from(`GET ${ENDPOINT} HTTP/1.1\r\nHost: x\r\nAuthorization: Bearer ${T}\r\nContent-Length: 4\r\nTransfer-Encoding: chunked\r\n\r\n0\r\n\r\nGET ${ENDPOINT} HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n`),
responses: [ok, c401],
audits: [T],
},
{ name: 'PROXY protocol preamble spoofing the source → Go 400', data: Buffer.concat([Buffer.from('PROXY TCP4 6.6.6.6 127.0.0.1 1111 2222\r\n'), req([`Authorization: Bearer ${T}`])]), responses: [go400], audits: [] },
{ name: 'foreign Host header → served normally', data: Buffer.from(`GET ${ENDPOINT} HTTP/1.1\r\nHost: evil.example:443\r\nAuthorization: Bearer ${T}\r\nConnection: close\r\n\r\n`), responses: [ok], audits: [T] },
{ name: 'absolute-form target to another host with a valid token → same endpoint, 200', data: req([`Authorization: Bearer ${T}`], { target: `http://evil.example${ENDPOINT}` }), responses: [ok], audits: [T] },
{ name: 'absolute-form target without credentials → 401', data: req([], { target: `http://evil.example${ENDPOINT}` }), responses: [c401], audits: [] },
{ name: 'Expect: 100-continue with a body → final response only, authenticated', data: req([`Authorization: Bearer ${T}`, 'Expect: 100-continue'], { body: 'ab', close: false }), responses: [ok], audits: [T] },
{ name: '1000 KiB extra header alongside a valid token (under the limit) → 200', data: req([`X-Pad: ${'p'.repeat(1000 * 1024)}`, `Authorization: Bearer ${T}`]), responses: [ok], audits: [T] },
{ name: 'headers over the 1 MiB limit → Go 431, never audited', data: req([`Authorization: Bearer ${T}`, `X-Pad: ${'p'.repeat(1100 * 1024)}`]), responses: [{ status: 431, go: true }], audits: [] },
{ name: '900 KiB bearer value → 401, value never logged', data: req([`Authorization: Bearer ${'Q'.repeat(900 * 1024)}`]), responses: [c401], audits: [] },
{ name: '490 header lines + valid token → 200', data: req([...Array.from({ length: 490 }, (_, i) => `X-H${i}: v`), `Authorization: Bearer ${T}`]), responses: [ok], audits: [T] },
{ name: '600 header lines (over Go\'s header count limit) → Go 431', data: req([`Authorization: Bearer ${T}`, ...Array.from({ length: 600 }, (_, i) => `X-H${i}: v`)]), responses: [{ status: 431, go: true }], audits: [] },
{ name: 'HTTP/1.0 without Host → 200', data: Buffer.from(`GET ${ENDPOINT} HTTP/1.0\r\nAuthorization: Bearer ${T}\r\n\r\n`), responses: [ok], audits: [T] },
{ name: 'HTTP/0.9-style request line → Go 400', data: Buffer.from(`GET ${ENDPOINT}\r\n\r\n`), responses: [go400], audits: [] },
{ name: 'unsupported HTTP version → Go 505', data: req([`Authorization: Bearer ${T}`], { version: 'HTTP/9.9' }), responses: [{ status: 505, go: true }], audits: [] },
{ name: 'lower-case method → 405 before authentication', data: req([`Authorization: Bearer ${T}`], { method: 'get' }), responses: [{ status: 405 }], audits: [] },
{ name: 'method with a separator character → Go 400', data: req([`Authorization: Bearer ${T}`], { method: 'G(ET' }), responses: [go400], audits: [] },
{ name: 'tab-separated request line → Go 400', data: Buffer.from(`GET\t${ENDPOINT}\tHTTP/1.1\r\nHost: x\r\nAuthorization: Bearer ${T}\r\n\r\n`), responses: [go400], audits: [] },
{ name: 'missing request target → Go 400', data: Buffer.from(`GET HTTP/1.1\r\nHost: x\r\n\r\n`), responses: [go400], audits: [] },
{ name: 'bare-LF line endings (tolerated by Go) → 200', data: Buffer.from(`GET ${ENDPOINT} HTTP/1.1\nHost: x\nAuthorization: Bearer ${T}\nConnection: close\n\n`), responses: [ok], audits: [T] },
{ name: '64 KiB request target → 404', data: req([`Authorization: Bearer ${T}`], { target: '/' + 'z'.repeat(64 * 1024) }), responses: [{ status: 404 }], audits: [] },
{ name: 'UTF-8 and Latin-1 bytes in unrelated header values → 200', data: Buffer.concat([Buffer.from(`GET ${ENDPOINT} HTTP/1.1\r\nHost: x\r\nX-U: \u{65e5}\u{672c}\r\nX-L: `), Buffer.from([0xe9, 0xff]), Buffer.from(`\r\nAuthorization: Bearer ${T}\r\nConnection: close\r\n\r\n`)]), responses: [ok], audits: [T] },
];
const audited: string[] = [];
let appResponses = 0;
for (const c of cases) {
await test.step(c.name, async () => {
const r = await raw(http, server, c.data, { expectResponses: c.responses.length });
expect(r.outcome, c.name).toBe('response');
expect(r.responses.map((x) => x.status), c.name).toEqual(c.responses.map((x) => x.status));
r.responses.forEach((res, i) => {
const e = c.responses[i];
if (e.go) expect(res.header('content-type'), `${c.name}: Go plain-text response`).toBe('text/plain; charset=utf-8');
else if (e.status === 200) expectContainersResponse(res);
else expectErrorResponse(res, e.status as 401 | 404 | 405);
if (!e.go) appResponses++;
expectNoTokenFragments(res.bodyText + JSON.stringify(res.headers), secrets, `${c.name} response`);
});
audited.push(...c.audits);
});
}
// the server survived everything
expectContainersResponse(await http.get(server.url(), { token: U }));
audited.push(U);
expect((await server.stop()).exitCode).toBe(0);
const logs = server.logs();
expect(logs.requests().length, 'Go-rejected requests never reach the app log').toBe(appResponses + 1);
expect(readOperations(server.dbPath).map((o) => o.token), 'exactly the authenticated requests were audited, in order').toEqual(audited);
expect(server.stderr).not.toContain('QQQQQQQQQQQQQQQQ');
expect(logs.count({ level: ['WARN', 'ERROR'] })).toBe(0);
expectNoTokenFragments(server.stderr, secrets, 'server log');
assertDbInvariants(server.dbPath, { tokenCount: 3, operationCount: audited.length });
});
test('path normalization tricks and URL-encoding variations: only the exact endpoint after percent-decoding is served, nothing redirects', async ({ cli, startServer, http, secrets }) => {
const T = await cli.tokenNew('paths');
const server = await startServer();
server.fakeDocker!.many(1);
const targets: Array<[string, 200 | 404 | 400]> = [
[ENDPOINT, 200],
['/api/v1/%63ontainers', 200],
['/%61%70%69/%76%31/%63%6f%6e%74%61%69%6e%65%72%73', 200],
['/api/v1/containers?x=../../etc/passwd', 200],
['/api/v1/containers?%00', 200],
['/api/v1/containers?', 200],
['/api/v1/containers?a=%zz', 200],
['http://localhost/api/v1/containers', 200],
['http://evil.example:1/api/v1/containers?x=1', 200],
['/api/v1/containers/', 404],
['/api/v1/containers/.', 404],
['/api/v1/./containers', 404],
['/api/v1/x/../containers', 404],
['/api/v1/x/%2e%2e/containers', 404],
['/api/v1/x/%2E%2E%2Fcontainers', 404],
['/api/v1%2Fcontainers', 404],
['/api%2Fv1%2Fcontainers', 404],
['/api/v1/%2563ontainers', 404],
['/api/v1/%c0%aecontainers', 404],
['/api/v1/containers%00', 404],
['/api/v1/containers%3F', 404],
['/api/v1/containers%23', 404],
['/api/v1/containers;jsessionid=1', 404],
['/api/v1/containers..;/', 404],
['/api/v1/containers#frag', 404],
['//api/v1/containers', 404],
['/api//v1/containers', 404],
['/API/v1/containers', 404],
['/api/v1/CONTAINERS', 404],
['/api/v1/%43ontainers', 404],
['/api/v1/\u{ff43}ontainers', 404],
['/api/v1/%EF%BD%83ontainers', 404],
['/api\\v1\\containers', 404],
['/api/v1/containers\\', 404],
['/api/v1/containers%20', 404],
['/api/v1/containers.', 404],
['*', 404],
['/api/v1/%u0063ontainers', 400],
['/api/v1/containers%', 400],
['/api/v1/containers%zz', 400],
];
let audits = 0;
let reached = 0;
for (const [target, status] of targets) {
await test.step(`${target} → ${status} (and 405 / 401 variants)`, async () => {
if (target.startsWith('/')) expect(routeOf(target), 'the oracle agrees with the table').toBe(status === 200 ? 'endpoint' : status === 404 ? 'other' : 'bad');
const variants: Array<[string, Buffer, number]> = [
['GET + token', req([`Authorization: Bearer ${T}`], { target }), status],
['POST + token', req([`Authorization: Bearer ${T}`], { target, method: 'POST' }), status === 200 ? 405 : status],
['GET, no token', req([], { target }), status === 200 ? 401 : status],
];
for (const [name, data, st] of variants) {
const res = onlyResponse(await raw(http, server, data), `${target} ${name}`);
expect(res.status, `${target} ${name}`).toBe(st);
expect(res.header('location'), 'never redirected').toBeUndefined();
if (st === 400) expect(res.header('content-type')).toBe('text/plain; charset=utf-8');
else {
reached++;
if (st === 200) expectContainersResponse(res);
else expectErrorResponse(res, st as 401 | 404 | 405);
}
if (st === 200) audits++;
expectNoTokenFragments(res.bodyText, secrets, 'response');
}
// curl --path-as-is agrees for targets curl can carry verbatim
if (/^\/[A-Za-z0-9\-._~%/?=&;:@!$'()*+,]*$/.test(target) && !target.includes('%00') && status !== 400) {
const c = await http.curl({ url: server.url(target), token: T, pathAsIs: true });
expect(c.exitCode).toBe(0);
expect(c.status, `curl ${target}`).toBe(status);
expect(c.headers['location']).toBeUndefined();
reached++;
if (status === 200) audits++;
}
});
}
expect((await server.stop()).exitCode).toBe(0);
expect(server.logs().requests().length).toBe(reached);
assertDbInvariants(server.dbPath, { tokenCount: 1, operationCount: audits });
});
test('SQL injection in every user-controlled field (descriptions, delete argument, bearer token, query): schema and rows intact, payloads stored verbatim', async ({ cli, ws, startServer, http, secrets }) => {
test.setTimeout(90_000);
const model = new StateModel();
const keep = model.add(await cli.tokenNew('keep'), 'keep').token;
const revoked = model.add(await cli.tokenNew('revoked'), 'revoked').token;
await cli.tokenDelete(revoked);
model.deactivate(revoked);
const attachTarget = ws.path('attached.db');
const payloads = [...SQL_PAYLOADS, `'; ATTACH DATABASE '${attachTarget}' AS x; CREATE TABLE x.t(a); --`, `${keep}' OR '1'='1`, `${keep}'--`, `' OR token LIKE '%`, `x'); UPDATE tokens SET active=1 WHERE token='${revoked}'; --`];
await test.step('descriptions: stored byte-exact, listed Go-quoted, nothing executed', async () => {
for (const p of payloads) model.add(await cli.tokenNew(p), p);
model.verifyDb(ws.dbPath);
const rows = await cli.tokenList();
model.verifyList(rows);
for (const p of payloads) expect(rows.some((r) => r.descriptionQuoted === goQuote(p)), `listed: ${p.slice(0, 40)}`).toBe(true);
});
await test.step('delete argument: every payload is "token not found", state byte-identical', async () => {
const before = stateDigest(ws.dbPath);
for (const p of payloads) {
const r = await cli.run(['token', 'delete', p]);
expectCliFailure(r, { exitCode: 1, message: 'token not found' });
expect(r.stderr.includes(keep), 'token never echoed').toBe(false);
}
expect(stateDigest(ws.dbPath)).toBe(before);
});
const server = await startServer({ logLevel: 'DEBUG' });
server.fakeDocker!.many(1);
await test.step('bearer token and query string: unknown token, never a match', async () => {
for (const p of payloads) {
const r = onlyResponse(await raw(http, server, req([`Authorization: Bearer ${p}`])), p);
expectErrorResponse(r, 401);
expectNoTokenFragments(r.bodyText, secrets, 'response');
const q = await http.get(server.url(`${ENDPOINT}?token=${encodeURIComponent(p)}&filter=${encodeURIComponent(p)}`), { token: keep });
expectContainersResponse(q);
model.audit(keep);
}
// the revoked token stays revoked despite the UPDATE payloads
expectErrorResponse(await http.get(server.url(), { token: revoked }), 401);
});
expect((await server.stop()).exitCode).toBe(0);
const classes = server.logs().filter({ msg: 'authentication result' }).map((r) => r.attrs.result);
expect(classes.filter((c) => c === 'unknown_token')).toHaveLength(payloads.length);
expect(classes.filter((c) => c === 'inactive_token')).toHaveLength(1);
expect(fs.existsSync(attachTarget), 'ATTACH payload was never executed').toBe(false);
expect(readSchema(ws.dbPath).tables).toEqual(['operations', 'tokens']);
expect(readToken(ws.dbPath, revoked)!.active).toBe(0);
model.verifyDb(ws.dbPath, 'all payloads stored verbatim, no row altered');
});
test('shell metacharacters, control characters, log/list forging and Unicode confusables in descriptions; NUL where the platform permits it', async ({ cli, ws, startServer, http, secrets, rng }) => {
test.setTimeout(90_000);
const model = new StateModel();
const canary = ws.path('pwned');
const shellish = [`$(touch ${canary})`, `\`touch ${canary}\``, `; touch ${canary}`, `| touch ${canary}`, `&& touch ${canary}`, `\ntouch ${canary}`, `'; touch ${canary}; '`, `"; touch ${canary}; "`, `\${IFS}touch\${IFS}${canary}`];
const forging = [
'x\ntime=2026-01-01T00:00:00.000Z level=ERROR msg="forged record" origin=6.6.6.6',
'\r\nlevel=ERROR msg=forged',
'" level=ERROR msg="forged',
'ok\x1b[2K\x1b[1Glevel=ERROR msg=forged\x1b[0m',
'a\tfalse\t2020-01-01T00:00:00Z\t2020-01-02T00:00:00Z',
'row\nFAKE.TOKEN\t""\ttrue\t2020-01-01T00:00:00Z\t-',
// forging the padded table: a closing quote and padded columns inside the description
'a" false 2020-01-01T00:00:00Z 2020-01-02T00:00:00Z',
'"' + ' '.repeat(40) + 'true 2020-01-01T00:00:00Z -',
'\x08\x08\x08\x08\x7f\x07\x0c\x0b',
'tab\there\u{2028}ls\u{2029}ps\u{85}nel',
];
const confusables = ['\u{430}dmin', 'admin', 'p\u{430}yp\u{430}l', '\u{202e}lanimret\u{202c}', '\u{ff21}\u{ff24}\u{ff2d}\u{ff29}\u{ff2e}', 'e\u{301}', '\u{e9}'];
await test.step('created through argv and through a real shell with quoting: stored verbatim, never executed', async () => {
for (const d of [...shellish, ...forging, ...confusables]) model.add(await cli.tokenNew(d), d);
for (const d of shellish) {
const r = await cli.shell(`"$DPSAPI_BIN" token new ${shellQuote(d)}`);
expectCliSuccess(r, { stderr: '' });
model.add(secrets.register(r.stdout.trim()), d);
}
expect(fs.existsSync(canary), 'no description was ever executed').toBe(false);
model.verifyDb(ws.dbPath);
});
await test.step('token list stays one strictly parseable row per token at INFO and at TRACE', async () => {
model.verifyList(await cli.tokenList());
model.verifyList(await cli.tokenListAll(), 'token list all', 'all');
// plain `token list` filters in SQL: one `row read` per active token only
const active = await cli.run(['token', 'list'], { env: { LOG_LEVEL: 'TRACE' } });
expectCliSuccess(active);
model.verifyList(parseTokenList(active.stdout), 'token list at TRACE');
expect(parseLogs(active.stderr).filter({ msg: 'row read' }), 'token list reads only the active rows').toHaveLength(model.active().length);
const r = await cli.run(['token', 'list', 'all'], { env: { LOG_LEVEL: 'TRACE' } });
expectCliSuccess(r);
model.verifyList(parseTokenList(r.stdout), 'token list all at TRACE', 'all');
const logs = parseLogs(r.stderr);
expect(logs.unparsed, 'every TRACE line is a single well-formed record').toEqual([]);
expect(logs.count({ level: 'ERROR' }), 'no forged ERROR record').toBe(0);
expect(logs.has({ msg: /forged/ })).toBe(false);
const read = logs.filter({ msg: 'row read' });
expect(read).toHaveLength(model.tokens.size);
for (const d of forging) expect(read.some((x) => x.attrs.description === d), `description logged as one quoted attribute: ${JSON.stringify(d)}`).toBe(true);
});
await test.step('TRACE server: authenticating with every token logs its description safely', async () => {
const server = await startServer({ logLevel: 'TRACE' });
for (const t of model.active()) {
expectContainersResponse(await http.get(server.url(), { token: t }));
model.audit(t);
}
expect((await server.stop()).exitCode).toBe(0);
const logs = server.logs();
expect(logs.unparsed).toEqual([]);
expect(logs.count({ level: 'ERROR' })).toBe(0);
expect(logs.has({ msg: /forged/ })).toBe(false);
expect(logs.requests().every((x) => x.attrs.origin === '127.0.0.1')).toBe(true);
expect(fs.existsSync(canary)).toBe(false);
});
await test.step('Unicode confusables are distinct tokens/descriptions: confusable token variants never match', async () => {
const target = model.active()[0];
const before = stateDigest(ws.dbPath);
for (const m of nearMatches(target, rng, 400).filter((x) => x.kind === 'confusable' || x.kind === 'zero-width').slice(0, 10)) {
expectCliFailure(await cli.run(['token', 'delete', m.value]), { exitCode: 1, message: 'token not found' });
}
expect(stateDigest(ws.dbPath)).toBe(before);
const rows = await cli.tokenList();
expect(rows.filter((r) => r.description === 'admin')).toHaveLength(1);
expect(rows.filter((r) => r.description === '\u{430}dmin')).toHaveLength(1);
});
await test.step('NUL: argv/env cannot carry it (platform), HTTP rejects or does not match it', async () => {
const before = stateDigest(ws.dbPath);
// execve(2) takes NUL-terminated strings: Node refuses to spawn, the binary never runs
await expect(cli.run(['token', 'new', 'a\x00b'])).rejects.toThrow(/harness:spawn|ERR_INVALID_ARG_VALUE|null bytes/);
await expect(cli.run(['token', 'list'], { env: { LOG_LEVEL: 'INFO\x00' } })).rejects.toThrow(/harness:spawn|ERR_INVALID_ARG_VALUE|null bytes/);
// a shell drops NUL from command substitution: the stored description has no NUL
const sh = await cli.shell(`"$DPSAPI_BIN" token new "$(printf 'a\\000b')"`);
expectCliSuccess(sh);
const tok = sh.stdout.trim();
expect(readToken(ws.dbPath, tok)!.descriptionHex).toBe('6162');
model.add(tok, 'ab');
expect(stateDigest(ws.dbPath)).not.toBe(before);
const server = await startServer();
const t = model.active()[0];
expect(onlyResponse(await raw(http, server, req([`Authorization: Bearer ${t}\x00`])), 'nul header').status).toBe(400);
expect(onlyResponse(await raw(http, server, req([`Authorization: Bearer ${t.slice(0, 20)}%00${t.slice(20)}`])), 'pct nul').status).toBe(401);
expectErrorResponse(onlyResponse(await raw(http, server, req([`Authorization: Bearer ${t}`], { target: `${ENDPOINT}%00` })), 'nul path'), 404);
expectContainersResponse(onlyResponse(await raw(http, server, req([`Authorization: Bearer ${t}`], { target: `${ENDPOINT}?a=%00` })), 'nul query'));
model.audit(t);
expect((await server.stop()).exitCode).toBe(0);
});
model.verifyDb(ws.dbPath, 'final state');
});
test('forwarding headers cannot spoof the audited origin (IPv4 and IPv6 listeners, 40 spoof variants each)', async ({ cli, ws, startServer, http, rng }) => {
const T = await cli.tokenNew('origin');
const spoofs = ['X-Forwarded-For', 'Forwarded', 'X-Real-IP', 'True-Client-IP', 'CF-Connecting-IP', 'X-Client-IP', 'X-Originating-IP', 'X-Remote-Addr', 'X-Remote-IP', 'Client-IP', 'X-Cluster-Client-IP', 'Fastly-Client-IP', 'X-ProxyUser-Ip', 'Via'];
const values = ['6.6.6.6', '6.6.6.6, 127.0.0.1', 'for=6.6.6.6;proto=https', 'for="[2001:db8::6]:1"', 'unknown', '::1', '127.0.0.1', '10.0.0.1, 10.0.0.2, 10.0.0.3', '<script>', `'; DROP TABLE operations; --`];
let total = 0;
for (const [addr, origin] of [['127.0.0.1', '127.0.0.1'], ['::1', '::1']] as const) {
const server = await startServer({ addr });
for (let i = 0; i < 40; i++) {
const lines = Array.from({ length: rng.int(1, 4) }, () => `${rng.pick(spoofs)}: ${rng.pick(values)}`);
expectContainersResponse(onlyResponse(await raw(http, server, req([...lines, `Authorization: Bearer ${T}`])), lines.join('; ')));
}
expect((await server.stop()).exitCode).toBe(0);
total += 40;
const ops = readOperations(server.dbPath);
expect(ops).toHaveLength(total);
expect(ops.slice(-40).every((o) => o.origin === origin), `every audited origin is the TCP peer ${origin}`).toBe(true);
expect(server.logs().requests().every((r) => r.attrs.origin === origin)).toBe(true);
}
assertDbInvariants(ws.dbPath, { tokenCount: 1, operationCount: 80 });
});
test('concurrent token deletion versus authenticated requests: no request started after a deletion succeeds; audit rows equal the 200s exactly', async ({ cli, ws, startServer, http, rng }) => {
test.setTimeout(90_000);
const victims = await Promise.all(Array.from({ length: 8 }, (_, i) => cli.tokenNew(`victim ${i}`)));
const bystanders = await Promise.all(Array.from({ length: 2 }, (_, i) => cli.tokenNew(`bystander ${i}`)));
const server = await startServer();
server.fakeDocker!.many(1);
interface Sample { token: string; start: number; end: number; endWall: number; status: number }
const samples: Sample[] = [];
const deletedAt = new Map<string, number>(); // performance.now() when `token delete` exited
const stopAfter = new Map<string, number>(); // requests still to send after deletion
let stopping = false;
const loop = async (token: string) => {
let after = 0;
while (!stopping) {
const start = performance.now();
const r = await http.get(server.url(), { token });
const end = performance.now();
samples.push({ token, start, end, endWall: Date.now(), status: r.status });
if (r.status === 200) expectContainersResponse(r);
else expectErrorResponse(r, 401);
if (deletedAt.has(token) && start > deletedAt.get(token)! && ++after >= (stopAfter.get(token) ?? 5)) return;
}
};
const guard = <T,>(p: Promise<T>) => p.catch((e) => {
stopping = true; // a failed worker must not leave the others running until the test timeout
throw e;
});
const loops = [...victims.flatMap((v) => Array.from({ length: 6 }, () => loop(v))), ...bystanders.map((b) => loop(b))].map(guard);
const deleter = (async () => {
for (const v of rng.shuffle(victims)) {
const warmup = rng.int(5, 30);
await pollUntil('some traffic for the victim', () => (stopping || samples.filter((s) => s.token === v).length >= warmup ? true : undefined), { timeoutMs: 20_000 });
if (stopping) return;
stopAfter.set(v, rng.int(3, 8));
await cli.tokenDelete(v);
deletedAt.set(v, performance.now());
}
await pollUntil('every victim loop finished', () => (stopping || victims.every((v) => samples.filter((s) => s.token === v && s.start > deletedAt.get(v)!).length >= 18) ? true : undefined), { timeoutMs: 30_000 });
stopping = true;
})();
const settled = await Promise.allSettled([guard(deleter), ...loops]);
const failed = settled.find((x) => x.status === 'rejected');
if (failed) throw (failed as PromiseRejectedResult).reason;
expect((await server.stop()).exitCode).toBe(0);
const ops = readOperations(ws.dbPath);
let racing = 0;
for (const v of victims) {
const mine = samples.filter((s) => s.token === v);
const del = deletedAt.get(v)!;
const row = readToken(ws.dbPath, v)!;
expect(row.active).toBe(0);
expect(mine.filter((s) => s.start > del && s.status !== 401), `${tokenId(v)}: requests started after the deletion returned must all be 401`).toEqual([]);
const oks = mine.filter((s) => s.status === 200);
expect(oks.every((s) => s.start < del), 'every 200 was started before the deletion completed').toBe(true);
const audits = ops.filter((o) => o.token === v);
expect(audits.length, `${tokenId(v)}: one audit row per 200, none for 401s`).toBe(oks.length);
// A request authenticated (read active=1) before the deactivation commit may insert its audit row
// after deleted_at: the app checks then inserts without a common transaction (README: authentication,
// then audit insert). The exact invariant is therefore bounded by the request itself: each audit row
// precedes the end of the last successful response.
const lastOkEnd = Math.max(...oks.map((s) => s.endWall), 0);
for (const a of audits) expect(rfc3339ToNanos(a.time) <= msToNanos(lastOkEnd + 1), 'audit row written before its response completed').toBe(true);
racing += audits.filter((a) => rfc3339ToNanos(a.time) > rfc3339ToNanos(row.deleted_at!)).length;
}
for (const b of bystanders) {
const mine = samples.filter((s) => s.token === b);
expect(mine.every((s) => s.status === 200), 'bystanders were never refused').toBe(true);
expect(ops.filter((o) => o.token === b)).toHaveLength(mine.length);
}
test.info().annotations.push({ type: 'race', description: `${samples.length} requests; ${racing} audit row(s) written after their token's deleted_at by requests authenticated before the commit` });
assertDbInvariants(ws.dbPath, { tokenCount: 10, operationCount: samples.filter((s) => s.status === 200).length });
});
test('forced interleaving: a request authenticated while a deactivation is queued is still answered 200 and audited exactly once (the row may postdate deleted_at)', async ({ cli, ws, startServer, http }) => {
test.setTimeout(60_000);
const server = await startServer({ logLevel: 'DEBUG' });
server.fakeDocker!.many(1);
let afterDeletion = 0;
let deleteReturnedFirst = 0;
const N = 6;
for (let i = 0; i < N; i++) {
await test.step(`iteration ${i}`, async () => {
const t = await cli.tokenNew(`interleave ${i}`);
// a test-owned write lock parks the request between its token lookup and its audit insert
const db = new DatabaseSync(ws.dbPath, { timeout: 2000 });
db.exec('BEGIN IMMEDIATE');
let released = false;
try {
let reqEnd = 0;
let delEnd = 0;
const reqP = http.get(server.url(), { token: t, timeoutMs: 20_000 }).then((x) => ((reqEnd = performance.now()), x));
await server.waitForLog({ msg: 'authentication result', attrs: { result: 'ok', token_id: tokenId(t) } });
// let the parked insert reach SQLite's long busy back-off (~100 ms sleeps), then queue the
// deletion just before releasing: its short early polls usually win the lock
await new Promise((res) => setTimeout(res, 1200));
const delP = cli.run(['token', 'delete', t]).then((x) => ((delEnd = performance.now()), x)); // BEGIN IMMEDIATE: queued behind the same lock
await new Promise((res) => setTimeout(res, 40));
db.exec('ROLLBACK');
db.close();
released = true;
const [res, del] = await Promise.all([reqP, delP]);
// the authentication decision was taken while the token was active: it stands
expectContainersResponse(res);
expect(del.exitCode).toBe(0);
if (delEnd < reqEnd) deleteReturnedFirst++;
} finally {
if (!released) {
db.exec('ROLLBACK');
db.close();
}
}
const row = readToken(ws.dbPath, t)!;
expect(row.active).toBe(0);
const audit = readOperations(ws.dbPath).filter((o) => o.token === t);
expect(audit, 'exactly one audit row for the one 200').toHaveLength(1);
if (rfc3339ToNanos(audit[0].time) > rfc3339ToNanos(row.deleted_at!)) afterDeletion++;
// and from now on the token is refused
expectErrorResponse(await http.get(server.url(), { token: t }), 401);
});
}
expect((await server.stop()).exitCode).toBe(0);
// Spec ambiguity (reported): authentication and the audit insert are not one transaction. Observed:
// `token delete` can return (exit 0) before an already-authenticated request gets its 200, and that
// request's audit row is committed after the deactivation — yet its `time` is stamped before the
// insert waits for the lock, so time <= deleted_at still holds here. Structural invariants hold either way.
test.info().annotations.push({ type: 'interleaving', description: `${afterDeletion}/${N} audit rows written after deleted_at; ${deleteReturnedFirst}/${N} times \`token delete\` returned before the 200 arrived` });
assertDbInvariants(ws.dbPath, { tokenCount: N, operationCount: N });
});
test('audit argument attacks: SQL injection, shell metacharacters, huge, control-character and confusable arguments are usage errors — nothing echoed, executed or written; audit reveals only the stored rows', async ({ cli, ws, startServer, http, secrets }) => {
test.setTimeout(90_000);
const model = new StateModel();
// descriptions that try to forge audit lines / extra columns (audit never prints descriptions)
const forged = `x\t127.0.0.1\tGET containers\n2026-01-01T00:00:00Z\tFAKE-TOKEN\t6.6.6.6\tGET containers`;
const T = model.add(await cli.tokenNew(forged), forged).token;
const R = model.add(await cli.tokenNew('revoked\nTIME\tTOKEN\tORIGIN\tOPERATION'), 'revoked\nTIME\tTOKEN\tORIGIN\tOPERATION').token;
const server = await startServer({ logLevel: 'DEBUG' });
for (const t of [T, R, T, R, T]) {
expectContainersResponse(await http.get(server.url(), { token: t }));
model.audit(t);
}
await cli.tokenDelete(R);
model.deactivate(R);
const canary = ws.path('pwned');
const attachTarget = ws.path('attached.db');
const payloads = [
...SQL_PAYLOADS,
'10; DROP TABLE operations',
'10 OR 1=1',
`10' OR '1'='1`,
'1); DELETE FROM operations; --',
'10 UNION SELECT token, description, active, created_at FROM tokens',
`1h'; ATTACH DATABASE '${attachTarget}' AS x; --`,
'10)--',
'(SELECT 10)',
`$(touch ${canary})`,
`\`touch ${canary}\``,
`10; touch ${canary}`,
`10 | touch ${canary}`,
`10 && touch ${canary}`,
'10\n11',
'10\r',
'10\x1b[2K\x1b[1G',
'\x1b]0;title\x07',
'10\x7f',
'\x0810',
'10‮',
'10',
'10s',
'10ѕ',
'1O',
'l0',
'10​',
'%s',
'%n',
'${jndi:ldap://x}',
'../../etc/passwd',
'-1',
'-9223372036854775808',
'9223372036854775808',
'18446744073709551615w',
'1' + '0'.repeat(200) + 's',
'9'.repeat(100_000),
'9'.repeat(131_071),
'w'.repeat(10_000),
];
for (const p of payloads) expect(auditArgsOutcome([p]), JSON.stringify(p.slice(0, 40))).toEqual({ ok: false, message: 'invalid audit argument' });
const usage = `dpsapi: usage error: invalid audit argument\n${USAGE_TEXT}`;
const wal = `${ws.dbPath}-wal`;
const before = { db: fileDigest(ws.dbPath), wal: fileDigest(wal), state: stateDigest(ws.dbPath) };
await test.step(`${payloads.length} hostile arguments through argv (server running)`, async () => {
await mapLimit(payloads, 8, async (p) => {
const r = await cli.run(['audit', p]);
expectCliFailure(r, { exitCode: 2, message: 'usage error: invalid audit argument' });
expect(r.stderr, 'exact stderr: the argument is never echoed').toBe(usage);
});
// more than one argument, also hostile
for (const extra of [['10', `; touch ${canary}`], ['1', "' OR '1'='1"], ['', ''], ['10', 'h', 'x']]) {
const r = await cli.run(['audit', ...extra]);
expect(r.stderr).toBe(`dpsapi: usage error: audit takes at most one argument\n${USAGE_TEXT}`);
expect(r.exitCode).toBe(2);
}
});
await test.step('through a real shell (quoted payloads, invalid UTF-8 bytes); NUL cannot reach argv', async () => {
for (const p of [`$(touch ${canary})`, `\`touch ${canary}\``, `10; touch ${canary}`, "10' OR '1'='1"]) {
const r = await cli.shell(`"$DPSAPI_BIN" audit ${shellQuote(p)}`);
expectCliFailure(r, { exitCode: 2, message: 'usage error: invalid audit argument' });
}
const bytes = await cli.shell(`"$DPSAPI_BIN" audit "$(printf '1\\377h')"`);
expect(bytes.stderr).toBe(usage);
expect(bytes.exitCode).toBe(2);
await expect(cli.run(['audit', '1\x00'])).rejects.toThrow(/harness:spawn|ERR_INVALID_ARG_VALUE|null bytes/);
});
await test.step('nothing executed, attached or written', async () => {
expect(fs.existsSync(canary), 'no payload was executed').toBe(false);
expect(fs.existsSync(attachTarget), 'no ATTACH was executed').toBe(false);
expect(fileDigest(ws.dbPath), 'database file bytes').toBe(before.db);
expect(fileDigest(wal), 'WAL bytes').toBe(before.wal);
expect(stateDigest(ws.dbPath)).toBe(before.state);
expect(readSchema(ws.dbPath).tables).toEqual(['operations', 'tokens']);
});
await test.step('a valid audit reveals exactly the stored rows: four columns, no descriptions, no forged lines; tokens only on stdout', async () => {
const r = await cli.run(['audit', '100'], { env: { LOG_LEVEL: 'TRACE' } });
expectCliSuccess(r);
expect(r.stdout).toBe(expectedAuditStdout(readOperations(ws.dbPath), { limit: 100n }));
const rows = parseAuditOutput(r.stdout);
expect(rows.map((x) => x.token)).toEqual([T, R, T, R, T]);
expect(r.stdout.includes('FAKE-TOKEN') || r.stdout.includes('6.6.6.6') || r.stdout.includes('revoked'), 'descriptions never printed').toBe(false);
expectNoTokenFragments(r.stderr, secrets, 'audit TRACE stderr');
expect(parseLogs(r.stderr).unparsed).toEqual([]);
});
expect((await server.stop()).exitCode).toBe(0);
expect(server.logs().requests(), 'no CLI invocation reached the server').toHaveLength(5);
model.verifyDb(ws.dbPath, 'after the audit attacks');
});
test('database lock contention: a foreign writer holding the write lock — writes wait up to the 5 s busy timeout then fail cleanly, readers never block, nothing is corrupted', async ({ cli, ws, startServer, http, secrets, e2e }) => {
test.setTimeout(90_000);
const model = new StateModel();
const T = model.add(await cli.tokenNew('lock'), 'lock').token;
const V = model.add(await cli.tokenNew('victim'), 'victim').token;
const server = await startServer({ logLevel: 'DEBUG' });
server.fakeDocker!.many(1);
expectContainersResponse(await http.get(server.url(), { token: T }));
model.audit(T);
e2e.trackedDbs.add(ws.dbPath);
// Test-owned writer connection (the only scenario allowed to open the app DB for writing).
// It never modifies anything: it only holds the RESERVED lock of a BEGIN IMMEDIATE transaction.
const holdLock = (ms: number): Promise<void> => {
const db = new DatabaseSync(ws.dbPath, { timeout: 2000 });
db.exec('BEGIN IMMEDIATE');
return new Promise((resolve) =>
setTimeout(() => {
db.exec('ROLLBACK');
db.close();
resolve();
}, ms),
);
};
await test.step('short lock (1.5 s): the request and a CLI write wait for it and then succeed', async () => {
const released = holdLock(1500);
const [r, created] = await Promise.all([http.get(server.url(), { token: T }), cli.run(['token', 'new', 'during short lock'])]);
await released;
expectContainersResponse(r);
expect(r.durationMs, 'the audit insert waited for the lock').toBeGreaterThan(1200);
model.audit(T);
expectCliSuccess(created, { stderr: '' });
expect(created.durationMs).toBeGreaterThan(1200);
model.add(created.stdout.trim(), 'during short lock');
});
await test.step('long lock (7 s): writes fail after ~5 s with clean errors; reads and unauthenticated requests are not blocked', async () => {
const before = stateDigest(ws.dbPath);
const released = holdLock(7000);
const t0 = performance.now();
const opsBefore = readOperations(ws.dbPath);
const [authed, created, deleted, listed, audited, unauth, unknown] = await Promise.all([
http.get(server.url(), { token: T, timeoutMs: 20_000 }),
cli.run(['token', 'new', 'during long lock']),
cli.run(['token', 'delete', V]),
cli.run(['token', 'list']),
cli.run(['audit', '100']),
http.get(server.url()),
http.get(server.url('/elsewhere'), { token: T }),
]);
const elapsed = performance.now() - t0;
await released;
// authenticated request: token lookup (a read) succeeds, the audit insert times out → 500
expectErrorResponse(authed, 500);
expectNoInternalDetails(authed.body);
expect(authed.durationMs, 'bounded by the 5 s busy timeout').toBeGreaterThan(4500);
expect(authed.durationMs).toBeLessThan(6900);
// CLI writes: exit 1 with a single clean error line, no token printed
expectCliFailure(created, { exitCode: 1, message: /^insert token: database is locked/ });
expectCliFailure(deleted, { exitCode: 1, message: /^begin transaction: database is locked|^query token: database is locked|^update token: database is locked/ });
for (const c of [created, deleted]) expect(c.durationMs).toBeGreaterThan(4500);
// readers: WAL lets them through immediately
expectCliSuccess(listed);
expect(listed.durationMs).toBeLessThan(2000);
model.verifyList(parseTokenList(listed.stdout));
expectCliSuccess(audited, { stdout: expectedAuditStdout(opsBefore, { limit: 100n }), stderr: '' });
expect(audited.durationMs, 'audit is a reader: not blocked by the write lock').toBeLessThan(2000);
expectErrorResponse(unauth, 401);
expect(unauth.durationMs).toBeLessThan(2000);
expectErrorResponse(unknown, 404);
expect(elapsed).toBeLessThan(9000);
expect(stateDigest(ws.dbPath), 'nothing was written while the lock was held').toBe(before);
expect(readToken(ws.dbPath, V)!.active, 'the failed deletion left the token active').toBe(1);
expectNoTokenFragments(authed.body + created.stderr + deleted.stderr, secrets, 'errors');
});
await test.step('after release everything works again', async () => {
expectContainersResponse(await http.get(server.url(), { token: T }));
model.audit(T);
await cli.tokenDelete(V);
model.deactivate(V);
expectErrorResponse(await http.get(server.url(), { token: V }), 401);
});
expect((await server.stop()).exitCode).toBe(0);
const errors = server.logs().filter({ level: 'ERROR' });
expect(errors.map((e) => e.msg)).toEqual(['audit insert failed']);
expect(errors[0].attrs.error).toMatch(/database is locked/);
expect(errors[0].attrs.token_id).toBe(tokenId(T));
model.verifyDb(ws.dbPath, 'after contention');
});
test('Docker socket unavailable and deliberate backend failures under concurrent attack traffic: generic 500s, audit still written, exact failure counts, recovery', async ({ cli, ws, startServer, http, secrets }) => {
test.setTimeout(90_000);
const T = await cli.tokenNew('backend');
let audits = 0;
const burst = async (server: ServerHandle, n: number) => {
const reqs = Array.from({ length: n }, (_, i) => (i % 2 === 0 ? { token: T } : { token: i % 4 === 1 ? 'x'.repeat(43) : undefined }));
return mapLimit(reqs, 16, (o) => http.get(server.url(), o));
};
for (const docker of ['missing', 'inaccessible'] as const) {
await test.step(`socket ${docker}: 60 concurrent requests`, async () => {
const server = await startServer({ docker });
const res = await burst(server, 60);
res.forEach((r, i) => {
expectErrorResponse(r, i % 2 === 0 ? 500 : 401);
expectNoInternalDetails(r.body);
});
audits += 30;
expect((await server.stop()).exitCode).toBe(0);
expect(server.logs().count({ level: 'ERROR', msg: 'docker request failed' })).toBe(30);
});
}
const server = await startServer();
const f = server.fakeDocker!;
const leaky = `boom at ${ws.dbPath} /var/run/docker.sock SELECT token FROM tokens WHERE token='${'Z'.repeat(43)}' goroutine 1 [running]`;
const cases: Array<[string, () => void, number]> = [
['Engine 500 carrying paths, SQL and a stack trace', () => f.error500(leaky), 500],
['Engine 401', () => f.error(401, 'unauthorized engine'), 500],
['Engine 404 (API version mismatch)', () => f.error(404, 'page not found'), 500],
['connection dropped', () => f.destroy(), 500],
['truncated JSON', () => f.badJson('[{"Id":"abc"'), 500],
['object instead of array', () => f.badJson('{"message":"x"}'), 500],
['wrong field types', () => f.badJson('[{"Id":1}]'), 500],
['empty body', () => f.badJson(''), 500],
// Observed, undefined by the spec: the Docker SDK decodes only the first JSON value of the body,
// so `null` and trailing garbage after a valid value are not client errors and yield 200 [].
['null body (observed: SDK treats it as no containers)', () => f.badJson('null'), 200],
['valid array followed by garbage (observed: trailing bytes ignored by the SDK)', () => f.badJson('[] trailing-garbage'), 200],
['slow Engine within the 10 s bound', () => f.empty().delay(1500), 200],
];
for (const [name, apply, status] of cases) {
await test.step(name, async () => {
apply();
const res = await burst(server, 20);
res.forEach((r, i) => {
if (i % 2 === 0) {
if (status === 200) expect(expectContainersResponse(r)).toEqual([]);
else expectErrorResponse(r, 500);
} else expectErrorResponse(r, 401);
expectNoInternalDetails(r.body);
expect(r.body).not.toContain('ZZZZZZZZ');
expect(Object.keys(r.headers).filter((h) => /^(api-version|docker-|ostype|server)$/i.test(h))).toEqual([]);
expectNoTokenFragments(r.body, secrets, name);
});
audits += 10;
f.delay(0);
});
}
await test.step('temporarily failing Engine under 16-way concurrency: exactly k failures', async () => {
f.many(2).flaky(7);
const res = await mapLimit(Array.from({ length: 24 }, () => 0), 16, () => http.get(server.url(), { token: T }));
expect(res.filter((r) => r.status === 500)).toHaveLength(7);
expect(res.filter((r) => r.status === 200)).toHaveLength(17);
audits += 24;
});
expect((await server.stop()).exitCode).toBe(0);
const errs = server.logs().filter({ level: 'ERROR' });
expect(errs.every((e) => e.msg === 'docker request failed')).toBe(true);
expect(errs).toHaveLength(8 * 10 + 7);
assertDbInvariants(ws.dbPath, { tokenCount: 1, operationCount: audits });
});
test('slow-body client pins a connection during shutdown: SIGTERM waits the documented 15 s, then exits 1 with the shutdown error; new connections are refused meanwhile; the database stays valid', async ({ cli, ws, startServer, http }) => {
test.setTimeout(60_000);
const T = await cli.tokenNew('slow');
const server = await startServer({ logLevel: 'DEBUG' });
expectContainersResponse(await http.get(server.url(), { token: T }));
// unauthenticated attacker: declares a 100 kB body, sends 10 bytes and keeps the connection open.
// The app answers 405, but before writing it Go drains the declared body of a keep-alive request
// (up to 256 KiB, bounded by the 30 s read timeout), so the connection stays active.
const stuck = http.raw({ host: server.connectHost, port: server.port, data: `POST ${ENDPOINT} HTTP/1.1\r\nHost: x\r\nContent-Length: 100000\r\n\r\n0123456789`, timeoutMs: 45_000, allowTimeout: true, waitForClose: true });
await server.waitForLog({ msg: 'this request has been made', attrs: { method: 'POST', status: 405 } });
const t0 = performance.now();
const stopping = server.stop('SIGTERM', { timeoutMs: 30_000 });
await pollUntil('listener closed', async () => ((await http.raw({ host: server.connectHost, port: server.port, data: 'GET / HTTP/1.1\r\nHost: x\r\n\r\n', timeoutMs: 2000, allowTimeout: true })).outcome === 'refused' ? true : undefined), { timeoutMs: 5000 });
const exit = await stopping;
const elapsed = performance.now() - t0;
expect(exit.exitCode, 'documented: in-flight requests not finished within 15 s → exit 1').toBe(1);
expect(exit.stderr.trimEnd().split('\n').pop()).toBe('dpsapi: http server shutdown: context deadline exceeded');
expect(elapsed).toBeGreaterThan(14_500);
expect(elapsed).toBeLessThan(20_000);
const s = await stuck;
expect(['closed', 'reset', 'response'], 'the stuck connection was torn down by the shutdown').toContain(s.outcome);
if (s.outcome === 'response') expect(s.response!.status).toBe(405);
assertDbInvariants(ws.dbPath, { tokenCount: 1, operationCount: 1 });
// a new server on the same database works normally
const again = await startServer({ dbPath: ws.dbPath });
expectContainersResponse(await http.get(again.url(), { token: T }));
expect((await again.stop()).exitCode).toBe(0);
});