// Middleware
app.use("*", requestIdMiddleware);
-app.use("*", cors());
+// Permissive CORS is applied to the proxy and mask APIs so browser-based clients
+// can call them, but NOT to the dashboard. The dashboard UI and its JSON APIs
+// (/dashboard, /dashboard/api/*) are served same-origin and may be
+// unauthenticated; a wildcard Access-Control-Allow-Origin there would let any
+// website the operator visits read logged request data cross-origin. Same-origin
+// dashboard use needs no CORS headers, so excluding it changes no legitimate flow.
+const corsMiddleware = cors();
+app.use("*", (c, next) =>
+ c.req.path === "/dashboard" || c.req.path.startsWith("/dashboard/")
+ ? next()
+ : corsMiddleware(c, next),
+);
app.use("*", logger());
// Favicon
// Environment variable password patterns: _PASSWORD or _PWD suffix with value (8+ chars)
// Case-insensitive for variable name, supports = and : assignment, quoted/unquoted values
+ // The variable-name run is length-bounded ({0,128}) so a long, non-matching
+ // run of word characters cannot force quadratic backtracking (ReDoS); real
+ // env var names are far shorter than this bound.
if (enabledTypes.has("ENV_PASSWORD")) {
const passwordPattern =
- /[A-Za-z_][A-Za-z0-9_]*(?:PASSWORD|_PWD)\s*[=:]\s*['"]?[^\s'"]{8,}['"]?/gi;
+ /[A-Za-z_][A-Za-z0-9_]{0,128}(?:PASSWORD|_PWD)\s*[=:]\s*['"]?[^\s'"]{8,}['"]?/gi;
detectPattern(text, passwordPattern, "ENV_PASSWORD", matches, locations);
}
// Environment variable secret patterns: _SECRET suffix with value (8+ chars)
// Case-insensitive for variable name, supports = and : assignment, quoted/unquoted values
+ // The variable-name run is length-bounded ({0,128}) to prevent quadratic
+ // backtracking (ReDoS) on long non-matching word-character runs.
if (enabledTypes.has("ENV_SECRET")) {
- const secretPattern = /[A-Za-z_][A-Za-z0-9_]*_SECRET\s*[=:]\s*['"]?[^\s'"]{8,}['"]?/gi;
+ const secretPattern = /[A-Za-z_][A-Za-z0-9_]{0,128}_SECRET\s*[=:]\s*['"]?[^\s'"]{8,}['"]?/gi;
detectPattern(text, secretPattern, "ENV_SECRET", matches, locations);
}
).join('') + '</div>';
}
+function escapeHtml(value) {
+ return String(value == null ? '' : value)
+ .replace(/&/g, '&')
+ .replace(/</g, '<')
+ .replace(/>/g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''');
+}
+
function formatSourceLabel(source) {
return source === 'browser_extension' ? 'Browser Extension' : source.toUpperCase();
}
'</td>' +
'<td class="text-sm px-4 py-3 border-b border-border-subtle align-middle">' + sourceBadge + '</td>' +
'<td class="text-sm px-4 py-3 border-b border-border-subtle align-middle">' + statusBadge + '</td>' +
- '<td class="font-mono text-[0.7rem] text-text-secondary px-4 py-3 border-b border-border-subtle align-middle">' + log.model + '</td>' +
+ '<td class="font-mono text-[0.7rem] text-text-secondary px-4 py-3 border-b border-border-subtle align-middle">' + escapeHtml(log.model) + '</td>' +
'<td class="text-sm px-4 py-3 border-b border-border-subtle align-middle">' +
(entities.length > 0
? '<div class="flex flex-wrap gap-1">' + entities.map(e => '<span class="font-mono text-[0.55rem] px-1.5 py-0.5 bg-elevated border border-border rounded-sm text-text-secondary">' + e.trim() + '</span>').join('') + '</div>'