65 lines
6 KiB
JavaScript
65 lines
6 KiB
JavaScript
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const { EventEmitter } = require('node:events');
|
|
const { PassThrough } = require('node:stream');
|
|
const { isPrivateIp } = require('../src/utils/urlSafety');
|
|
const { decodeBase64, inspect, download, MAX_BYTES } = require('../src/utils/generatedImageStorage');
|
|
const { args, budgetLimit } = require('../src/utils/generatedImages');
|
|
const png = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aL9sAAAAASUVORK5CYII=', 'base64');
|
|
test('mapped IPv6, noncanonical IPs, internal/special networks and transition ranges fail closed', () => {
|
|
for (const ip of ['::ffff:127.0.0.1','::ffff:7f00:1','0:0:0:0:0:ffff:a00:1','::ffff:8.8.8.8','64:ff9b::a00:1','2002:7f00:1::','2001:db8::1','fe90::1','fec0::1','ff02::1','::1','10.0.0.1','100.64.0.1','169.254.169.254','172.16.0.1','192.0.0.1','192.0.2.1','198.18.1.1','198.51.100.1','203.0.113.2','127.1','0x7f000001','2130706433']) assert.equal(isPrivateIp(ip), true, ip);
|
|
assert.equal(isPrivateIp('8.8.8.8'), false); assert.equal(isPrivateIp('2606:4700:4700::1111'), false);
|
|
});
|
|
test('base64, byte bound, MIME/magic, assembled argument fields and budget types are strict', () => {
|
|
assert.equal(decodeBase64(png.toString('base64')).mime, 'image/png');
|
|
for (const text of ['', 'a', '!!!!', png.toString('base64') + '\n', png.toString('base64').replace(/=$/, ''), 'eHh4']) assert.throws(() => decodeBase64(text));
|
|
assert.throws(() => inspect(Buffer.alloc(MAX_BYTES + 1))); assert.throws(() => inspect(png, 'text/html'));
|
|
for (const value of [false, {}, [1000], 999, 32001, '1.5']) assert.throws(() => budgetLimit(value));
|
|
assert.equal(budgetLimit(null), 32000);
|
|
for (const input of [null, [], { prompt: 'ok', owner: 7 }, {prompt: 'ok', model: 'paid'}, {prompt: 'ok', layout: 'https://example.test'}]) assert.throws(() => args(input));
|
|
});
|
|
test('URL download pins vetted DNS results into HTTPS lookup, never forwards credentials or follows redirect, enforces size', async () => {
|
|
let calls = 0;
|
|
const lookup = async () => [{ address:'8.8.8.8',family:4 }];
|
|
function request(status = 200, body = png, headers = {}) {
|
|
return (url, options, callback) => {
|
|
calls++; assert.equal(url.hostname, 'provider-assets.test'); assert.equal(options.agent, false); assert.equal(options.headers.Authorization, undefined);
|
|
options.lookup(url.hostname, {all:true}, (err, result) => { assert.equal(err,null); assert.deepEqual(result,[{address:'8.8.8.8',family:4}]); });
|
|
const req = new EventEmitter(); req.destroy = err => req.emit('error',err);
|
|
process.nextTick(() => { const res = new PassThrough(); res.statusCode=status; res.headers={'content-type':'image/png',...headers}; callback(res); if (!res.destroyed) res.end(body); }); return req;
|
|
};
|
|
}
|
|
const result = await download('https://provider-assets.test/image',{lookup,request:request()}); assert.deepEqual(result.bytes,png);
|
|
for (const url of ['http://provider-assets.test/x','https://user:secret@provider-assets.test/x','https://provider-assets.test:8443/x']) await assert.rejects(download(url,{lookup,request:request()}));
|
|
await assert.rejects(download('https://provider-assets.test/x',{lookup:async()=>[{address:'8.8.8.8',family:4},{address:'::ffff:127.0.0.1',family:6}],request:request()}));
|
|
assert.equal(calls,1);
|
|
await assert.rejects(download('https://provider-assets.test/x',{lookup,request:request(302,png,{location:'https://127.0.0.1/'})}));
|
|
await assert.rejects(download('https://provider-assets.test/x',{lookup,request:request(200,png,{'content-length':String(MAX_BYTES+1)})}));
|
|
await assert.rejects(download('https://provider-assets.test/x',{lookup,request:request(200,Buffer.alloc(MAX_BYTES+1))}));
|
|
});
|
|
test('storage preflight verifies read permission as well as writes before authorizing payment', async () => {
|
|
const fs = require('node:fs'); const vm = require('node:vm'); const sdk = require('@aws-sdk/client-s3');
|
|
const commands = [];
|
|
const module = { exports: {} };
|
|
vm.runInNewContext(fs.readFileSync('src/utils/generatedImageStorage.js', 'utf8'), { module, Buffer, URL, require(name) {
|
|
if (name === 'fs') return { readFileSync: () => 'synthetic-only' };
|
|
if (name === './urlSafety') return { isPrivateIp };
|
|
if (name === '@aws-sdk/client-s3') return { ...sdk, S3Client: class {
|
|
constructor(options) { assert.equal(options.forcePathStyle, true); }
|
|
async send(command) { commands.push(command.constructor.name); if (command instanceof sdk.HeadObjectCommand) throw Error('synthetic denied read'); return {}; }
|
|
} };
|
|
return require(name);
|
|
} });
|
|
const store = module.exports.createStorage({ GENERATED_IMAGES_S3_ENDPOINT: 'http://synthetic.invalid', GENERATED_IMAGES_S3_ACCESS_KEY_FILE: '/synthetic/access', GENERATED_IMAGES_S3_SECRET_KEY_FILE: '/synthetic/secret' });
|
|
await assert.rejects(store.ready(), /denied read/);
|
|
assert.deepEqual(commands, ['HeadBucketCommand', 'PutObjectCommand', 'HeadObjectCommand', 'DeleteObjectCommand']);
|
|
});
|
|
test('public 2001 IPv6 and mixed public A/AAAA remain usable by images and Nextcloud/WebDAV', async () => {
|
|
const fs=require('fs'),vm=require('vm');
|
|
for(const ip of ['2001:4860:4860::8888','2001:200::1','2001:db7::1','2001:db9::1','3fff:1000::1']) assert.equal(isPrivateIp(ip),false,ip);
|
|
for(const ip of ['2001::1','2001:1ff:ffff::1','2001:2::1','2001:10::1','2001:20::1','2001:db8::1','2002::1','3ffe::1','3fff::1','3fff:fff::1','64:ff9b:1::1','fc00::1']) assert.equal(isPrivateIp(ip),true,ip);
|
|
let addresses=[{address:'8.8.8.8',family:4},{address:'2001:4860:4860::8888',family:6}];
|
|
const module={exports:{}};vm.runInNewContext(fs.readFileSync('src/utils/urlSafety.js','utf8'),{module,URL,require:n=>n==='dns'?{promises:{lookup:async()=>addresses}}:require(n)});
|
|
assert.equal((await module.exports.assertSafeHttpsUrl('https://nextcloud.synthetic.test/files')).hostname,'nextcloud.synthetic.test');
|
|
addresses=addresses.concat({address:'2001:db8::1',family:6});await assert.rejects(module.exports.assertSafeHttpsUrl('https://nextcloud.synthetic.test/files'),/private IP/);
|
|
});
|