[grade=B] DC-081: Input validation for 20 highest-risk mutating routes
CI / Test & Lint (push) Canceled after 0s
CI / Security audit (push) Canceled after 0s

Secures 20 mutating routes across 7 files against path traversal, shell
injection, and ReDoS vectors:
- containers.js: container ID validation + resource limit bounds (6 routes)
- recipes/manage.js: recipe ID slug validation (4 routes)
- tailscale.js: subdomain regex before interpolation + shell char blocking (2)
- workflows.js: workflow ID slug validation (3 routes)
- dependencies.js: service ID + dependsOn array validation (3 routes)
- logs.js: YYYY-MM-DD date format validation (1 route)
- sites.js: additional domain validation (1 route)

Uses existing REGEX patterns from constants.js. No new dependencies.
Codex: B (no blocking issues, 4 Low follow-ups for tests + strict bools).
1552/1552 tests pass, 0 regressions.
This commit is contained in:
Hermes
2026-08-12 06:20:48 -07:00
parent 37b2630525
commit 388a1fe487
7 changed files with 146 additions and 3 deletions
+46 -1
View File
@@ -1,9 +1,49 @@
const express = require('express');
const { DOCKER } = require('../src/utilities/constants');
const { paginate, parsePaginationParams } = require('../src/utilities/pagination');
const { NotFoundError } = require('../src/utilities/errors');
const { NotFoundError, ValidationError } = require('../src/utilities/errors');
const { success } = require('../src/utils/responses');
/**
* Validate a Docker container identifier (ID or name).
* Allows hex container IDs and Docker-compliant names.
* Blocks path traversal and shell metacharacters.
* @param {string} id - Container ID or name from route param
* @throws {ValidationError} if the ID is malformed
*/
function validateContainerId(id) {
if (!id || typeof id !== 'string') {
throw new ValidationError('Container ID is required');
}
// Docker names: [a-zA-Z0-9][a-zA-Z0-9_.-]*
// Docker IDs: 64-char hex — also matches the above pattern
// Max 128 chars covers IDs and names
if (!/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}$/.test(id)) {
throw new ValidationError('Invalid container ID format');
}
}
/**
* Validate numeric resource limits for container update.
* @param {*} memory - Memory in MB (optional)
* @param {*} cpus - CPU count (optional)
* @throws {ValidationError} if values are out of range
*/
function validateResourceLimits(memory, cpus) {
if (memory !== undefined) {
const memNum = Number(memory);
if (isNaN(memNum) || memNum < 0 || memNum > 1048576) {
throw new ValidationError('Memory must be a number between 0 and 1048576 MB');
}
}
if (cpus !== undefined) {
const cpuNum = Number(cpus);
if (isNaN(cpuNum) || cpuNum < 0 || cpuNum > 1024) {
throw new ValidationError('CPUs must be a number between 0 and 1024');
}
}
}
/**
* Containers route factory
* @param {Object} deps - Explicit dependencies
@@ -18,6 +58,7 @@ module.exports = function({ docker, log, asyncHandler, workflowEngine }) {
// Helper: verify container exists before operating on it
async function getVerifiedContainer(id) {
validateContainerId(id);
const container = docker.client.getContainer(id);
try {
await container.inspect();
@@ -205,6 +246,10 @@ module.exports = function({ docker, log, asyncHandler, workflowEngine }) {
router.put('/:id/resources', asyncHandler(async (req, res) => {
const container = await getVerifiedContainer(req.params.id);
const { memory, cpus } = req.body;
// Validate resource limits before applying to Docker
validateResourceLimits(memory, cpus);
const updateConfig = {};
if (memory !== undefined) {