Some checks failed
Quality gates / quality (pull_request) Failing after 2m39s
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).
87 lines
4.1 KiB
JavaScript
87 lines
4.1 KiB
JavaScript
// Container/polyglot rejection contract.
|
|
//
|
|
// The requirement is malformed/polyglot REJECTION, not "the re-encoder happens
|
|
// to drop the tail". Rejection must come from canonical parsing of the image
|
|
// container — the declared structure must consume exactly the supplied bytes —
|
|
// so it cannot be evaded by changing keyword casing or archive flavour, and it
|
|
// must not fire on arbitrary compressed bytes that merely contain those
|
|
// sequences inside legal image structure.
|
|
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { readFile } from 'node:fs/promises';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
import { 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 = (name) => readFile(fixture(name));
|
|
const dataUrl = (bytes, mime = 'image/jpeg') => `data:${mime};base64,${bytes.toString('base64')}`;
|
|
|
|
const TRAILING_DATA_FIXTURES = [
|
|
['ingress-tail-upper-script.jpg', 'uppercase <SCRIPT> tail'],
|
|
['ingress-tail-mixed-script.jpg', 'mixed-case <ScRiPt> tail'],
|
|
['ingress-tail-html.jpg', 'generic appended HTML with no script tag'],
|
|
['ingress-tail-zip-eocd.jpg', 'ZIP end-of-central-directory tail'],
|
|
['ingress-tail-zip-local.jpg', 'ZIP local-header tail'],
|
|
['ingress-tail-rar.jpg', 'RAR archive tail'],
|
|
['ingress-tail-7z.jpg', '7z archive tail'],
|
|
['ingress-tail-gzip.jpg', 'gzip member tail'],
|
|
['ingress-tail-single-nul.jpg', 'single appended NUL byte'],
|
|
];
|
|
|
|
test('appended trailing data is rejected regardless of casing or container flavour', async () => {
|
|
for (const [name, description] of TRAILING_DATA_FIXTURES) {
|
|
const bytes = await readFixture(name);
|
|
await assert.rejects(
|
|
() => validateImageIngress({ consent: true, imageDataUrl: dataUrl(bytes) }),
|
|
(error) => {
|
|
assert.match(error.message, /not a supported image|corrupt or malformed/i,
|
|
`${description} must be rejected with a sanitized ingress error`);
|
|
return true;
|
|
},
|
|
`${description} must not pass ingress`,
|
|
);
|
|
}
|
|
});
|
|
|
|
test('trailing data after a PNG IEND chunk is rejected', async () => {
|
|
const bytes = await readFixture('ingress-tail-after-iend.png');
|
|
await assert.rejects(
|
|
() => validateImageIngress({ consent: true, imageDataUrl: dataUrl(bytes, 'image/png') }),
|
|
/not a supported image|corrupt or malformed/i,
|
|
);
|
|
});
|
|
|
|
test('rejection is structural, not substring scanning: legal images carrying archive and script byte sequences are accepted', async () => {
|
|
// A JPEG COM segment legally contains arbitrary bytes. These exact sequences
|
|
// are what the old scanner searched for; canonical parsing must still accept.
|
|
const buried = await readFixture('ingress-buried-signatures.jpg');
|
|
assert.ok(buried.includes(Buffer.from('PK\x03\x04')), 'fixture must contain a ZIP local header sequence');
|
|
assert.ok(buried.includes(Buffer.from('PK\x05\x06')), 'fixture must contain a ZIP EOCD sequence');
|
|
assert.ok(buried.includes(Buffer.from('<script')), 'fixture must contain a lowercase script sequence');
|
|
const result = await validateImageIngress({ consent: true, imageDataUrl: dataUrl(buried) });
|
|
assert.equal(result.format, 'jpeg');
|
|
assert.ok(result.bytes > 0);
|
|
|
|
// High-entropy compressed data must not be misclassified either.
|
|
const entropy = await readFixture('ingress-entropy-control.jpg');
|
|
const entropyResult = await validateImageIngress({ consent: true, imageDataUrl: dataUrl(entropy) });
|
|
assert.equal(entropyResult.format, 'jpeg');
|
|
});
|
|
|
|
test('the provider is never reached for any trailing-data polyglot', async () => {
|
|
for (const [name] of TRAILING_DATA_FIXTURES) {
|
|
const bytes = await readFixture(name);
|
|
let providerCalled = false;
|
|
await assert.rejects(
|
|
() => analyzePhoto({
|
|
payload: { consent: true, imageDataUrl: dataUrl(bytes) },
|
|
config: { model: 'm', baseUrl: 'http://127.0.0.1:9/v1' },
|
|
fetchImpl: async () => { providerCalled = true; throw new Error('provider reached'); },
|
|
}),
|
|
);
|
|
assert.equal(providerCalled, false, `${name} must fail before any provider fetch`);
|
|
}
|
|
});
|