fix(security,reliability): address code-review findings
HIGH
- async handlers: Express 4 does not catch rejected promises and Node 22
exits on unhandled rejections — a malformed body ({} to /cropper/dequeue
etc.) could hang a request or kill the process. Every cropper route is
now wrapped (throw → 500 via next), request bodies are validated up
front (400), and JSON.parse of queue items is guarded. Server-wide
error middleware added, honouring err.status (413 for oversized bodies).
- error pages HTML-escape all interpolated values — the login failure page
rendered upstream (portal) error text raw.
MEDIUM
- outbound calls carry AbortSignal timeouts (5s OpenFGA verify, 15s
cropper upstreams) — a hung upstream can no longer pin a request.
- body limits scoped: /cropper gets 50mb (picture data URLs), everything
else 1mb (previously 50mb globally, memory-DoS surface pre-auth).
- cropper save validates the client-supplied domain against the catalog;
picture fetches validate the Minio object key (id-domains/ prefix, no ..).
LOW
- static assets served before the session middleware (no PocketBase load
per asset); landing header builds DOM nodes with textContent instead of
innerHTML; picture load checks upstream ok.
5 new tests (40 total) covering malformed bodies, malformed queue items,
unknown domains, object-key traversal, and error-page escaping.
This commit is contained in:
@@ -11,7 +11,7 @@ import {
|
||||
} from './helpers.js';
|
||||
|
||||
// Fake YOUreka portal: web-auth session + openfga verify.
|
||||
async function startFakePortal({ cookieStatus = 'Activated', allowed = true } = {}) {
|
||||
async function startFakePortal({ cookieStatus = 'Activated', allowed = true, authErrorMessage } = {}) {
|
||||
return startServer((req, res) => {
|
||||
let raw = '';
|
||||
req.on('data', (c) => (raw += c));
|
||||
@@ -23,6 +23,9 @@ async function startFakePortal({ cookieStatus = 'Activated', allowed = true } =
|
||||
if (req.method === 'POST' && req.url === '/api/web/auth') {
|
||||
const body = JSON.parse(raw || '{}');
|
||||
if (!body.returnUrl) return json(200, { ok: false, message: 'returnUrl is required' });
|
||||
if (authErrorMessage) {
|
||||
return json(200, { ok: false, message: authErrorMessage });
|
||||
}
|
||||
return json(200, { ok: true, sessionId: 's1', loginUrl: 'https://portal.example/login?return=1' });
|
||||
}
|
||||
if (req.method === 'GET' && req.url.startsWith('/api/web/auth/s1/cookie')) {
|
||||
@@ -263,3 +266,17 @@ test('proxy validation: auth, otp, and domain are enforced before contacting a2l
|
||||
await stop();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
test('upstream error text is HTML-escaped on the error page', async () => {
|
||||
const { base, stop } = await bootApp({}, { authErrorMessage: '<script>alert(1)</script>portal said no' });
|
||||
try {
|
||||
const res = await fetch(`${base}/auth/login`, manual);
|
||||
assert.equal(res.status, 502);
|
||||
const html = await res.text();
|
||||
assert.ok(!html.includes('<script>alert(1)</script>'), 'script tag must not be interpolated raw');
|
||||
assert.match(html, /<script>alert\(1\)<\/script>/);
|
||||
} finally {
|
||||
await stop();
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user