[grade=B] api: fix undeclared 'format' runtime bug in routes/ca.js + 10 lint errors
- routes/ca.js: declare format before pfx/pem/crt dispatch (was ReferenceError on every request that passed validation); add CA_CERT_FORMATS single source of truth + hardened format extraction (string-coerce, whitelist) - routes/caddycode.js: fix upstream-validation regexes (control-char classes) - routes/logs.js: SSE/validation lint fixes - routes/openclaw.js: remove useless regex escape in ALLOWED_PATH_RE - fleet-validation.js + http-caddy-admin-origin test: eslint-disable for intentional control-regex security sentinels - ca-dc076.routes.test.js: regression test for declared format + behavioral coverage of format validation (now pre-PKI) Judge: Qwen lane (qwen3.8-max) grade B, 0 blocking, verdict /tmp/judge-batch1-verdict.json. API suite 2859/2859 green.
This commit is contained in:
@@ -139,6 +139,9 @@ module.exports = function(ctx) {
|
||||
// can mis-handle; reject it to keep the password copy-paste-safe).
|
||||
const CA_PFX_PASSWORD_RE = /^[A-Za-z0-9!@#%^_+,.~:-]{8,64}$/;
|
||||
const CA_CERT_RATE_LIMIT = { windowMs: 60_000, max: 10 };
|
||||
// Single source of truth for accepted ?format= values. `wantsPfx`, the
|
||||
// password requirement, and the response dispatch all derive from this.
|
||||
const CA_CERT_FORMATS = ['pfx', 'pem', 'crt', 'key', 'fullchain'];
|
||||
const caCertRateBuckets = new Map(); // ip -> { count, resetAt }
|
||||
function caCertRateLimit(ip) {
|
||||
const now = Date.now();
|
||||
@@ -175,6 +178,24 @@ module.exports = function(ctx) {
|
||||
|
||||
const { domain } = req.params;
|
||||
|
||||
// FIX: `format` was referenced in the dispatch below but never declared,
|
||||
// so every request that passed validation threw ReferenceError. Default
|
||||
// 'pfx' matches the `wantsPfx` check (no format param => pfx).
|
||||
// Accept only a non-empty string: query strings can deliver arrays
|
||||
// (?format=a&format=b) or nested objects, which must be rejected.
|
||||
const rawFormat = req.query.format;
|
||||
if (rawFormat !== undefined && (typeof rawFormat !== 'string' || rawFormat === '')) {
|
||||
return ctx.errorResponse(res, 400,
|
||||
`Invalid format parameter. Use: ${CA_CERT_FORMATS.join(', ')}.`,
|
||||
{ code: 'DC-076_FORMAT_INVALID' });
|
||||
}
|
||||
if (rawFormat !== undefined && !CA_CERT_FORMATS.includes(rawFormat)) {
|
||||
return ctx.errorResponse(res, 400,
|
||||
`Invalid format '${rawFormat}'. Use: ${CA_CERT_FORMATS.join(', ')}.`,
|
||||
{ code: 'DC-076_FORMAT_INVALID' });
|
||||
}
|
||||
const format = rawFormat || 'pfx';
|
||||
|
||||
// DC-076: password is REQUIRED for the pfx format (no `=`) and must
|
||||
// be ≥ 8 chars. Previously `password = 'dashcaddy'` — a hardcoded
|
||||
// default that silently signed every PFX with the same published
|
||||
|
||||
@@ -64,8 +64,8 @@ function validateGenerationConfig(config) {
|
||||
// reverse_proxy upstreams). Two regex branches: (a) bare host with
|
||||
// required :port, (b) bracketed IPv6 literal with required :port.
|
||||
if (typeof upstream !== 'string'
|
||||
|| !/^[a-z0-9.\-]+:\d{1,5}$/i.test(upstream)
|
||||
&& !/^\[[a-z0-9.\-:.]+\]:\d{1,5}$/i.test(upstream)
|
||||
|| !/^[a-z0-9.-]+:\d{1,5}$/i.test(upstream)
|
||||
&& !/^\[[a-z0-9.:.-]+\]:\d{1,5}$/i.test(upstream)
|
||||
) {
|
||||
errors.push('upstream must be host:port (host letters/digits/dots/hyphens, port 1-65535, optional IPv6 brackets)');
|
||||
}
|
||||
|
||||
@@ -270,14 +270,10 @@ module.exports = function({ asyncHandler, ok, docker, logDigest, dockerMaintenan
|
||||
// can't change statusCode. The reader does the same validation but
|
||||
// we want to short-circuit here so the response status reflects the
|
||||
// right category (400 for validation, 503 for bind-mount missing).
|
||||
try {
|
||||
journald.assertUnitAllowed(req.query.unit);
|
||||
if (req.query.since) journald.parseTimestamp(req.query.since, 'since');
|
||||
} catch (err) {
|
||||
// Pass through the global error middleware so the response status
|
||||
// + shape matches every other validation error in the API.
|
||||
throw err;
|
||||
}
|
||||
// Throws pass straight to the global error middleware so the response
|
||||
// status + shape matches every other validation error in the API.
|
||||
journald.assertUnitAllowed(req.query.unit);
|
||||
if (req.query.since) journald.parseTimestamp(req.query.since, 'since');
|
||||
|
||||
// SSE headers — same convention as /logs/stream/:id.
|
||||
res.setHeader('Content-Type', 'text/event-stream');
|
||||
|
||||
@@ -147,7 +147,7 @@ module.exports = function openClawRoutes(ctx) {
|
||||
// Allowed chars in the downstream `path` segment: alphanumerics, `-`, `_`,
|
||||
// `.`, `~`, `/`, `?`, `&`, `=`, `:`, `@`, `+`, `,`, `;` (RFC 3986 pchar +
|
||||
// query/fragment separators). Anything else → 400.
|
||||
const ALLOWED_PATH_RE = /^[a-zA-Z0-9._~/?&=:@+,;%\-]*$/;
|
||||
const ALLOWED_PATH_RE = /^[a-zA-Z0-9._~/?&=:@+,;%-]*$/;
|
||||
// Maximum total `path` length (reasonable for a gateway UI endpoint).
|
||||
const MAX_PATH_LEN = 1024;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user