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:
+38
-7
@@ -70,7 +70,7 @@ function errorPage(res, status, title, message, actions = {}) {
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>${title} · YOUreka Tools</title>
|
||||
<title>${esc(title)} · YOUreka Tools</title>
|
||||
<link rel="stylesheet" href="/assets/style.css">
|
||||
</head>
|
||||
<body class="app">
|
||||
@@ -90,10 +90,10 @@ function errorPage(res, status, title, message, actions = {}) {
|
||||
<main class="container narrow center-v">
|
||||
<div class="card message-card">
|
||||
<div class="message-icon error">${ICONS[icon] || ICONS.alert}</div>
|
||||
<h1>${title}</h1>
|
||||
<p class="muted">${message}</p>
|
||||
<a class="btn btn-primary" href="${primary.href}">${primary.label}</a>
|
||||
${secondary ? `<a class="btn btn-ghost" href="${secondary.href}">${secondary.label}</a>` : ''}
|
||||
<h1>${esc(title)}</h1>
|
||||
<p class="muted">${esc(message)}</p>
|
||||
<a class="btn btn-primary" href="${esc(primary.href)}">${esc(primary.label)}</a>
|
||||
${secondary ? `<a class="btn btn-ghost" href="${esc(secondary.href)}">${esc(secondary.label)}</a>` : ''}
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
@@ -107,6 +107,16 @@ const ICONS = {
|
||||
'<svg viewBox="0 0 24 24" width="28" height="28" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>',
|
||||
};
|
||||
|
||||
/** HTML-escape interpolated values — error pages may carry upstream text. */
|
||||
function esc(value) {
|
||||
return String(value ?? '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
};
|
||||
|
||||
// ── app factory ──────────────────────────────────────────────────────────────
|
||||
export function createApp(overrides = {}) {
|
||||
const cfg = {
|
||||
@@ -127,7 +137,9 @@ export function createApp(overrides = {}) {
|
||||
const app = express();
|
||||
app.disable('x-powered-by');
|
||||
app.set('trust proxy', 1); // https termination upstream → correct Secure-cookie flag
|
||||
app.use(express.json({ limit: '50mb' })); // cropper pictures travel as data URLs
|
||||
// cropper pictures travel as data URLs (large); everything else stays small
|
||||
app.use('/cropper', express.json({ limit: '50mb' }));
|
||||
app.use(express.json({ limit: '1mb' }));
|
||||
|
||||
const setSessionCookie = (req, res, value) =>
|
||||
res.cookie('session', value, {
|
||||
@@ -147,6 +159,9 @@ export function createApp(overrides = {}) {
|
||||
maxAge: SESSION_MAX_AGE_MS,
|
||||
});
|
||||
|
||||
// ── static assets (no session work per asset) ─────────────────────────────
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
// ── session middleware: recover / refresh the YOUreka Auth session ────────
|
||||
app.use(async (req, res, next) => {
|
||||
req.session = null;
|
||||
@@ -181,7 +196,6 @@ export function createApp(overrides = {}) {
|
||||
};
|
||||
|
||||
// ── static assets & landing page ──────────────────────────────────────────
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
// ── auth: YOUreka portal web-auth flow ────────────────────────────────────
|
||||
app.get('/auth/login', async (req, res) => {
|
||||
@@ -339,6 +353,7 @@ export function createApp(overrides = {}) {
|
||||
createCropperRouter({
|
||||
...cropperClients,
|
||||
a2loftBase: cfg.a2loftBase,
|
||||
domainCodes,
|
||||
removebgUrl: cfg.removebgUrl,
|
||||
noderedBase: cfg.noderedBase,
|
||||
...cropGateContext,
|
||||
@@ -411,6 +426,22 @@ export function createApp(overrides = {}) {
|
||||
}
|
||||
});
|
||||
|
||||
// ── error handler (async handlers are wrapped; this catches next(err)) ─────
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
app.use((err, req, res, next) => {
|
||||
const status = err.status || err.statusCode || 500;
|
||||
if (status >= 500) console.error('[error]', req.method, req.path, err && err.message);
|
||||
if (res.headersSent) return next(err);
|
||||
const message =
|
||||
status === 413 ? 'The request is too large.'
|
||||
: status >= 500 ? 'An unexpected error occurred. Please try again.'
|
||||
: err.message || 'The request could not be processed.';
|
||||
if (req.path === '/cropper' || req.path.startsWith('/auth/')) {
|
||||
return errorPage(res, status, status === 413 ? 'Request too large' : 'Something went wrong', message);
|
||||
}
|
||||
res.status(status).json({ ok: false, message });
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user