Files
dpsapi/e2e/tests/host/persona-malicious-user.spec.ts
bryanpedini a139f6793d 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.
2026-10-06 10:52:25 +02:00

936 lines
53 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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);
});