46 lines
2.6 KiB
JavaScript
46 lines
2.6 KiB
JavaScript
|
|
// The browser app is served only on its own hostname, only from web/, with a
|
||
|
|
// CSP that keeps the page on its origin. Signed S3 requests are never caught.
|
||
|
|
import test from 'node:test';
|
||
|
|
import assert from 'node:assert/strict';
|
||
|
|
import fs from 'node:fs';
|
||
|
|
import os from 'node:os';
|
||
|
|
import path from 'node:path';
|
||
|
|
import http from 'node:http';
|
||
|
|
import { fileURLToPath } from 'node:url';
|
||
|
|
import { createHosted } from '../server.js';
|
||
|
|
|
||
|
|
const web = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'web');
|
||
|
|
const get = (port, p, headers = {}) => new Promise((resolve, reject) => {
|
||
|
|
http.get({ host: '127.0.0.1', port, path: p, headers }, (res) => { let b = ''; res.on('data', (d) => { b += d; }); res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body: b })); }).on('error', reject);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('app files: own host only, no escapes, strict CSP', async (t) => {
|
||
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'pithos-app-route-'));
|
||
|
|
const port = 18700 + Math.floor(Math.random() * 200);
|
||
|
|
const h = await createHosted({
|
||
|
|
listen: `127.0.0.1:${port}`, publicUrl: `http://127.0.0.1:${port}`, root, s3dBinary: null, portBase: 23000, maxRunning: 1, idleMinutes: 1,
|
||
|
|
gate: { invites: [] }, app: { dir: web, hosts: ['pithos.example'] },
|
||
|
|
}, () => {});
|
||
|
|
t.after(async () => { await h.close(); fs.rmSync(root, { recursive: true, force: true }); });
|
||
|
|
|
||
|
|
const index = await get(port, '/', { host: 'pithos.example' });
|
||
|
|
assert.equal(index.status, 200);
|
||
|
|
assert.match(index.headers['content-type'], /text\/html/);
|
||
|
|
assert.match(index.headers['content-security-policy'], /default-src 'self'; script-src 'self'/);
|
||
|
|
assert.match(index.headers['content-security-policy'], /frame-ancestors 'none'/);
|
||
|
|
assert.equal(index.headers['x-content-type-options'], 'nosniff');
|
||
|
|
assert.match(index.body, /\/_app\/app\.js/);
|
||
|
|
|
||
|
|
assert.equal((await get(port, '/_app/lib/crypt.js', { host: 'pithos.example' })).status, 200);
|
||
|
|
// the S3 host does not serve the app
|
||
|
|
assert.notEqual((await get(port, '/', { host: 's3.example' })).status, 200);
|
||
|
|
// no way out of web/, no dotfiles, no unknown types
|
||
|
|
for (const p of ['/_app/../package.json', '/_app/%2e%2e/package.json', '/_app/..%2fpackage.json', '/_app/.hidden.js', '/_app/lib', '/_app/nope.exe']) {
|
||
|
|
assert.equal((await get(port, p, { host: 'pithos.example' })).status, 404, p);
|
||
|
|
}
|
||
|
|
// a signed S3 request for "/" is S3, not the app
|
||
|
|
const s3 = await get(port, '/', { host: 'pithos.example', authorization: 'AWS4-HMAC-SHA256 Credential=AKIDUNKNOWN/20261005/us-east-1/s3/aws4_request, SignedHeaders=host, Signature=00' });
|
||
|
|
assert.notEqual(s3.status, 200);
|
||
|
|
assert.doesNotMatch(s3.body, /<html/);
|
||
|
|
});
|