// An attachment has to be the thing it says it is. The MIME type used to be
// taken on trust here, so anything could be posted as image/png: stored in the
// saved chat, and handed to a provider as a data URI. Documents and S3 uploads
// were always sniffed; this was the path that was not.
const test = require('node:test');
const assert = require('node:assert/strict');
const { validateAttachments } = require('../src/utils/clinicalConversation');
const b64 = buf => Buffer.from(buf).toString('base64');
const PNG = Buffer.from('89504e470d0a1a0a0000000d49484452', 'hex');
const JPEG = Buffer.from('ffd8ffe000104a464946', 'hex');
const WEBP = Buffer.concat([Buffer.from('RIFF'), Buffer.alloc(4), Buffer.from('WEBP'), Buffer.alloc(4)]);
const attach = (mimeType, buf) => validateAttachments([{ mimeType, dataBase64: b64(buf) }]);
const refuses = (mimeType, buf, why) => assert.throws(
() => attach(mimeType, buf),
err => { assert.match(err.message, /not the image type it claims to be/); return true; }, why);
test('a genuine image of each accepted type passes', () => {
assert.equal(attach('image/png', PNG).length, 1);
assert.equal(attach('image/jpeg', JPEG).length, 1);
assert.equal(attach('image/webp', WEBP).length, 1);
});
test('executable and script payloads labelled as images are refused', () => {
refuses('image/png', '', 'php');
refuses('image/png', '#!/bin/sh\nrm -rf /', 'shell script');
refuses('image/jpeg', '', 'html/js');
refuses('image/png', Buffer.from('4d5a90000300', 'hex'), 'a Windows executable');
refuses('image/png', Buffer.from('7f454c46', 'hex'), 'an ELF binary');
});
test('a real file of the wrong type is refused, not just junk', () => {
refuses('image/png', '%PDF-1.4 trailing', 'a PDF called a PNG');
refuses('image/png', Buffer.from('504b0304', 'hex'), 'a zip/docx called a PNG');
refuses('image/webp', PNG, 'a PNG called a WebP');
refuses('image/jpeg', PNG, 'a PNG called a JPEG');
});
test('a polyglot that merely starts with image bytes is still only that image', () => {
// A PNG header followed by script text sniffs as PNG and is accepted — which
// is correct: it IS a PNG. The protection is that it is never executed and
// never served as anything but an image, not that payloads cannot be hidden
// inside valid image bytes, which no sniffer can promise.
assert.equal(attach('image/png', Buffer.concat([PNG, Buffer.from('')])).length, 1);
});
test('the size and count limits still hold, and are checked before the bytes are read', () => {
assert.throws(() => validateAttachments(new Array(5).fill({ mimeType: 'image/png', dataBase64: b64(PNG) })),
/maximum of 4 images/);
const big = Buffer.concat([PNG, Buffer.alloc(5 * 1024 * 1024)]);
assert.throws(() => attach('image/png', big), /limited to 5 MiB/);
});
test('a type outside the allowlist never reaches the sniffer', () => {
assert.throws(() => attach('application/pdf', Buffer.from('%PDF-1.4')),
/Only PNG, JPEG and WebP/);
assert.throws(() => attach('image/svg+xml', Buffer.from('')),
/Only PNG, JPEG and WebP/);
});