// SPDX-License-Identifier: GPL-2.0-or-later // Copyright (C) 2026 Bryan Joshua Pedini /** * Compact malicious/fuzz pass against the final image (PLAN §12 malicious/fuzzer personas, image * side): SQL-injection-like and shell-metacharacter descriptions and token arguments through * `docker exec` (no shell exists in the image, so nothing can be interpreted — `docker diff` * proves no file was created), a long description, deterministic random descriptions, hostile * Authorization/forwarding headers and path tricks from outside the container, then invariant * checks on a DB snapshot and normal operations. */ import { test, expect, assertDbInvariants, buildRawRequest, dockerCli, expectCliFailure, expectCliSuccess, expectContainersResponse, expectErrorResponse, expectNoInternalDetails, goQuote, parseLogs, parseTokenList, readOperations, readTokens, } from '../../harness'; import { execApp, expectedOrigins, hex } from './image-helpers'; test('abuse through docker exec and HTTP from outside: verbatim storage, no shell, no bypass, invariants hold, normal operation continues', async ({ image, http, fuzz, secrets }) => { test.setTimeout(120_000); const c = await image.start({ suffix: 'abuse' }); c.fakeDocker!.many(2); const victim = (await c.exec(['token', 'new', 'victim'])).stdout.trim(); // ---- CLI: hostile descriptions ---- const hostile: string[] = [ `'); DROP TABLE tokens; --`, `x' OR '1'='1`, `"; DELETE FROM operations; UPDATE tokens SET active=1; --`, `$(touch /pwned)`, '`touch /pwned2`', '; rm -rf / ; echo owned > /app', '| cat /etc/passwd > /data/leak', '&& /bin/sh -c id', '%s%n%x %!v', '\x1b[31mred\x1b[0m\r\x07', 'Веаrеr confusable zero‑width​', 'x'.repeat(100_000), ]; // deterministic random descriptions (printable ASCII + some Unicode; no NUL — not representable in argv) const alphabet = [...`abcXYZ019 '"\`$;|&<>(){}[]*?~!#%\\/\t\n`, 'é', '☃', '日', '🚀', '​']; for (let i = 0; i < 6; i++) { const len = fuzz.rng.int(0, 64); hostile.push(fuzz.record({ kind: 'description', i, value: Array.from({ length: len }, () => fuzz.rng.pick(alphabet)).join('') }).value); } const stored = new Map([[victim, 'victim']]); for (const [i, d] of hostile.entries()) { await test.step(`token new hostile #${i}`, async () => { const r = await c.exec(['token', 'new', d]); expectCliSuccess(r, { stdout: /^[A-Za-z0-9_-]{43}\n$/, stderr: '' }); stored.set(r.stdout.trim(), d); }); } // ---- CLI: hostile token arguments ---- const flip = (s: string) => s.replace(/[a-z]/, (ch) => ch.toUpperCase()); const badDeletes: Array<[string, string]> = [ [`' OR '1'='1`, 'token not found'], ['%', 'token not found'], ['*', 'token not found'], [`${victim}' --`, 'token not found'], [`${victim} `, 'token not found'], [`\n${victim}`, 'token not found'], [flip(victim), 'token not found'], [victim.slice(0, 42), 'token not found'], ['', 'token not found'], ['$(id)', 'token not found'], ]; for (const [arg, message] of badDeletes) { await test.step(`token delete ${JSON.stringify(arg.slice(0, 12))}…`, async () => { const r = await execApp(c, ['token', 'delete', arg]); expectCliFailure(r, { exitCode: 1, message }); if (arg) expect(r.stderr.includes(arg), 'argument never echoed').toBe(false); }); } expectCliFailure(await execApp(c, ['token', 'delete', victim, '; rm -rf /']), { exitCode: 2, message: 'usage error: token delete requires exactly one argument' }); expectCliFailure(await execApp(c, ['token', 'new', 'a', '$(id)']), { exitCode: 2, message: 'usage error: token new takes at most one argument' }); // nothing outside the mounts changed in the container filesystem: no /pwned, /app untouched const diff = await dockerCli(['diff', c.name]); // As observed: Docker reports the mount points it created itself (/data, /var/run/docker.sock) expect(diff.stdout.trim().split('\n').sort(), 'docker diff').toEqual(['A /data', 'A /var', 'A /var/run', 'A /var/run/docker.sock']); // ---- HTTP from outside: hostile authorization and paths ---- let authenticated = 0; await test.step('hostile Authorization values → 401, no details', async () => { const values = [ `Bearer ' OR '1'='1`, `Bearer ${victim}' OR '1'='1`, 'Bearer %', 'Bearer *', `Bearer ${flip(victim)}`, `Bearer ${victim.slice(0, 42)}`, `Bearer ${victim}x`, `Bearer ${victim}`, `Bearer ${victim}, Bearer ${victim}`, `Bearer\t${victim}`, `Token ${victim}`, 'Bearer ' + 'A'.repeat(8192), ]; for (const v of values) { const r = await http.curl({ url: c.url(), headers: [`Authorization: ${v}`] }); expect(r.exitCode).toBe(0); expectErrorResponse(r, 401); expectNoInternalDetails(r.body); } // duplicated header and CRLF header-injection attempt over a raw socket const { hostname, port } = new URL(c.baseURL); const dup = await http.raw({ host: hostname, port: Number(port), data: buildRawRequest({ target: '/api/v1/containers', headers: [`Authorization: Bearer ${victim}`, `Authorization: Bearer ${victim}`] }) }); expectErrorResponse(dup.response!, 401); const inj = await http.raw({ host: hostname, port: Number(port), data: buildRawRequest({ target: '/api/v1/containers', headers: [`Authorization: Bearer x\r\nAuthorization: Bearer ${victim}`] }) }); expectErrorResponse(inj.response!, 401); }); await test.step('forwarding headers cannot spoof the audited origin; path tricks', async () => { const r = await http.curl({ url: c.url(), token: victim, headers: ['X-Forwarded-For: 203.0.113.9', 'Forwarded: for=203.0.113.9', 'X-Real-IP: 203.0.113.9'] }); expectContainersResponse(r); authenticated++; const enc = await http.curl({ url: c.url('/api/v1/%63ontainers?x=%27--'), token: victim }); expectContainersResponse(enc); authenticated++; for (const p of ['/api/v1/containers/', '//api/v1/containers', '/api/v1/./containers', '/api/v1/../v1/containers', '/api/v1%2Fcontainers', '/API/V1/CONTAINERS']) { const x = await http.curl({ url: c.url(p), token: victim, pathAsIs: true }); expectErrorResponse(x, 404); } expectErrorResponse(await http.curl({ url: c.url(), token: victim, method: 'DELETE' }), 405); }); const origins = await expectedOrigins(c); // ---- invariants on a snapshot ---- await test.step('database invariants after the abuse', async () => { const snap = await c.snapshotDb(); const rows = readTokens(snap); expect(rows.length).toBe(stored.size); for (const r of rows) expect(r.descriptionHex, 'stored verbatim').toBe(hex(stored.get(r.token)!)); expect(rows.every((r) => r.active === 1), 'no hostile input deactivated (or reactivated) anything').toBe(true); const ops = readOperations(snap); expect(ops).toHaveLength(authenticated); for (const o of ops) { expect(o.token).toBe(victim); expect(origins, 'origin is the TCP peer, never 203.0.113.9').toContain(o.origin); } assertDbInvariants(snap, { tokenCount: stored.size, operationCount: authenticated }); // token list stays one strictly parseable line per token whatever the descriptions contain const list = await c.exec(['token', 'list']); expectCliSuccess(list, { stderr: '' }); const parsed = parseTokenList(list.stdout); expect(parsed).toHaveLength(stored.size); for (const p of parsed) expect(p.descriptionQuoted).toBe(goQuote(stored.get(p.token)!)); }); // ---- normal operation continues ---- await test.step('normal administration afterwards', async () => { const fresh = (await c.exec(['token', 'new', 'after abuse'])).stdout.trim(); expect(secrets.has(fresh)).toBe(true); expect(expectContainersResponse(await http.get(c.url(), { token: fresh }))).toHaveLength(2); authenticated++; expectCliSuccess(await c.exec(['token', 'delete', victim]), { stdout: '', stderr: '' }); expectErrorResponse(await http.get(c.url(), { token: victim }), 401); expectContainersResponse(await http.get(c.url(), { token: fresh })); authenticated++; }); const exit = await c.stop(); expect(exit.exitCode).toBe(0); const logs = parseLogs(exit.stderr); expect(logs.unparsed).toEqual([]); expect(logs.count({ level: 'ERROR' })).toBe(0); secrets.assertNoSecretsIn(exit.stderr, 'docker logs'); const snap = await c.snapshotDb(); assertDbInvariants(snap, { tokenCount: stored.size + 1, operationCount: authenticated, auditBeforeDeletion: true }); });