timmy-talking-turd/tests/image-ingress.test.js
Timmy 517c8dbac3
Some checks failed
Quality gates / quality (pull_request) Failing after 2m39s
Harden image ingress: pinned runtime, header-bomb rejection, concurrency ceiling, bounded rate limiter, canonical data-URL/polyglot contract, 503-on-unavailable, body-read timeout
Closes the PR 63 hostile-review blockers:
1. Immutable pinned Python/Pillow runtime (TIMMY_PYTHON absolute, --verify-pin),
   deployment/runtime re-encode smoke gate in build_release + deploy_staging.
2. Header-only width/height/total-pixel/bomb rejection before full decode; proves
   6000x6000 and 12000x12000 stay resource bounded (RSS + address-space caps).
3. Fail-fast decoder concurrency ceiling; tests count actual spawned children.
4. Rate-limiter key cardinality hard-bounded under 20k+ unexpired identities,
   trusted-loopback-proxy identity, no spoofable forwarded headers, fixed-window
   boundary burst smoothed by two-window sliding count.
5. build_release explicitly syntax/gates every new JS module + Python re-encoder +
   production-runtime smoke; CI runs reencode-image test and the runtime pin smoke.
6. Robust polyglot contract via canonical container parsing; rejects uppercase/mixed
   script, appended HTML, ZIP local/EOCD and archive tails, data after canonical
   JPEG/PNG/WebP end; no naive compressed-byte scans (false-positive controls pass).
7. Canonical base64 data-URL grammar with byte-exact round trip; rejects missing/excess
   padding, whitespace/CRLF, malformed and noncanonical encodings; exact MIME policy.
8. Missing Pillow / runtime-unavailable maps to sanitized 503 + manual fallback.
9. Inbound body-read timeout and stop-on-oversize; preserves sanitized 413, base path,
   provider suppression, and temp cleanup; socket torn down on rejection.

Audited prior partial edits: reused the sound source modules, re-wired new tests into
the unit/syntax gates, fixed a non-canonical readJson that destroyed the socket before
delivering 413/408, and hardened test flakiness (port reuse, EPIPE, startup races).
2026-08-22 23:22:42 +00:00

171 lines
6.9 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { readFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import {
MAX_IMAGE_BYTES,
MAX_IMAGE_DIMENSION,
sniffImageFormat,
validateImageIngress,
} from '../src/image-ingress.js';
import { analyzePhoto } from '../src/vision-service.js';
const fixture = (name) => fileURLToPath(new URL(`../tests/fixtures/${name}`, import.meta.url));
const readFixture = async (name) => await readFile(fixture(name));
function dataUrl(bytes, mime) {
return `data:${mime};base64,${bytes.toString('base64')}`;
}
async function ingestFixture(name, mime = 'image/jpeg', overrides = {}) {
const bytes = await readFixture(name);
return validateImageIngress({
imageDataUrl: dataUrl(bytes, mime),
consent: true,
...overrides,
});
}
test('clean synthetic JPEG passes ingress and is re-encoded without metadata', async () => {
const result = await ingestFixture('ingress-clean.jpg');
assert.equal(result.format, 'jpeg');
assert.ok(result.bytes <= MAX_IMAGE_BYTES);
assert.match(result.imageDataUrl, /^data:image\/jpeg;base64,/);
const exifCount = execFileSync('python3', [
'-c',
'from PIL import Image;import sys,base64;print(len(Image.open(__import__("io").BytesIO(base64.b64decode(sys.argv[1]))).getexif()))',
result.imageDataUrl.split(',')[1],
], { encoding: 'utf8' });
assert.equal(exifCount.trim(), '0');
});
test('magic bytes are verified independent of declared MIME', async () => {
// HTML payload wearing a JPEG content type must be rejected.
await assert.rejects(
() => ingestFixture('ingress-spoofed.html'),
/not a supported image/i,
);
// A real PNG declared as JPEG must still be accepted by sniffing, not by MIME.
const png = await ingestFixture('ingress-metadata.png', 'image/jpeg');
assert.equal(png.originalFormat, 'png');
assert.equal(png.format, 'jpeg');
});
test('polyglot payloads are rejected', async () => {
await assert.rejects(() => ingestFixture('ingress-polyglot.gif'), /not a supported image|rejected|unsafe/i);
await assert.rejects(() => ingestFixture('ingress-zip-polyglot.jpg'), /rejected|unsafe|corrupt|malformed|not a supported image/i);
});
test('malformed and truncated images fail closed with sanitized errors', async () => {
for (const [name, mime] of [['ingress-garbage.jpg', 'image/jpeg'], ['ingress-empty.jpg', 'image/jpeg']]) {
await assert.rejects(() => ingestFixture(name, mime), (error) => {
assert.match(error.message, /upload a jpeg, png, or webp photo/i);
return true;
});
}
// A truncated JPEG has no EOI, so canonical container parsing rejects it
// before any decode or subprocess spawn. Either sanitized ingress error is
// acceptable; leaking anything else is not.
await assert.rejects(() => ingestFixture('ingress-truncated.jpg'), (error) => {
assert.match(error.message, /not a supported image|corrupt or malformed/i);
return true;
});
});
test('decompression bombs and oversized dimensions fail before provider work', async () => {
await assert.rejects(() => ingestFixture('ingress-bomb.png'), /too large|dimensions/i);
await assert.rejects(() => ingestFixture('ingress-oversized.jpg'), /too large|dimensions/i);
assert.equal(MAX_IMAGE_DIMENSION <= 4096, true);
});
test('EXIF and GPS metadata are stripped from the re-encoded image', async () => {
const result = await ingestFixture('ingress-exif.jpg');
assert.equal(result.metadataStripped, true);
const check = execFileSync('python3', ['-c',
'from PIL import Image;import sys,base64,io;'
+ 'im=Image.open(io.BytesIO(base64.b64decode(sys.argv[1])));'
+ 'ex=im.getexif();'
+ 'gps=ex.get_ifd(0x8825);'
+ 'print("MAKE" if ex.get(0x010F) else "CLEAN", "GPS" if gps else "CLEAN")',
result.imageDataUrl.split(',')[1],
], { encoding: 'utf8' });
assert.equal(check.trim(), 'CLEAN CLEAN');
const pngResult = await ingestFixture('ingress-metadata.png');
const pngCheck = execFileSync('python3', ['-c',
'from PIL import Image;import sys,base64,io;'
+ 'im=Image.open(io.BytesIO(base64.b64decode(sys.argv[1])));'
+ 'info=getattr(im,"text",{}) or {};'
+ 'print("TEXT" if info else "CLEAN", im.format.lower())',
pngResult.imageDataUrl.split(',')[1],
], { encoding: 'utf8' });
assert.equal(pngCheck.trim(), 'CLEAN jpeg');
});
test('body limit rejects oversized base64 bodies before decoding', async () => {
const huge = Buffer.alloc(MAX_IMAGE_BYTES + 1024, 65);
await assert.rejects(
() => validateImageIngress({ imageDataUrl: dataUrl(huge, 'image/jpeg'), consent: true }),
/under \d+ mb/i,
);
});
test('consent is still required after hardening', async () => {
const bytes = await readFixture('ingress-clean.jpg');
await assert.rejects(
() => validateImageIngress({ imageDataUrl: dataUrl(bytes, 'image/jpeg') }),
/consent/i,
);
});
test('SVG uploads never pass ingress regardless of extension', async () => {
await assert.rejects(
() => ingestFixture('ingress-script.svg', 'image/svg+xml'),
/not a supported image/i,
);
});
test('errors are sanitized: no image bytes, no base64, no stack in messages', async () => {
const cases = [];
for (const name of ['ingress-spoofed.html', 'ingress-polyglot.gif', 'ingress-truncated.jpg', 'ingress-bomb.png']) {
try { await ingestFixture(name); } catch (error) { cases.push(error.message); }
}
for (const message of cases) {
assert.doesNotMatch(message, /[A-Za-z0-9+/]{40,}/);
assert.doesNotMatch(message, /at\s+\S+\s+\(/);
assert.ok(message.length < 200);
}
});
test('rate limiter allows a bounded burst then fails closed with sanitized retry message', async () => {
const { createRateLimiter } = await import('../src/rate-limiter.js');
const limiter = createRateLimiter();
const first = limiter.take('client-a', Date.now());
assert.equal(first.allowed, true);
for (let i = 0; i < limiter.limit - 1; i += 1) {
assert.equal(limiter.take('client-a', Date.now()).allowed, true);
}
const blocked = limiter.take('client-a', Date.now());
assert.equal(blocked.allowed, false);
assert.match(blocked.reason, /try again later|slow down/i);
assert.ok(blocked.retryAfterMs > 0);
assert.doesNotMatch(blocked.reason, /image|payload|byte/i);
// A different client key is unaffected.
assert.equal(limiter.take('client-b', Date.now()).allowed, true);
});
test('analyzePhoto routes through hardened ingress before provider fetch', async () => {
const bytes = await readFixture('ingress-polyglot.gif');
let providerCalled = false;
await assert.rejects(
() => analyzePhoto({
payload: { consent: true, imageDataUrl: dataUrl(bytes, 'image/gif') },
config: { model: 'm', baseUrl: 'http://127.0.0.1:9/v1' },
fetchImpl: async () => { providerCalled = true; throw new Error('provider reached'); },
}),
/not a supported image|rejected|unsafe/i,
);
assert.equal(providerCalled, false);
});