[grade=B] DC-081: Input validation for 20 highest-risk mutating routes
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:
@@ -18,6 +18,34 @@ const express = require('express');
|
||||
const { success, error: errorResponse } = require('../src/utils/responses');
|
||||
const { NotFoundError, ValidationError } = require('../src/utilities/errors');
|
||||
|
||||
/**
|
||||
* Validate a service ID for use in dependency lookups and config updates.
|
||||
* @param {string} serviceId - Service ID from route param
|
||||
* @throws {ValidationError} if the ID contains unsafe characters
|
||||
*/
|
||||
function validateServiceId(serviceId) {
|
||||
if (!serviceId || typeof serviceId !== 'string') {
|
||||
throw new ValidationError('Service ID is required');
|
||||
}
|
||||
if (!/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,100}$/.test(serviceId)) {
|
||||
throw new ValidationError('Invalid service ID format');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate each entry in a dependsOn array.
|
||||
* @param {Array} dependsOn - Array of dependency service IDs
|
||||
* @throws {ValidationError} if any entry is malformed
|
||||
*/
|
||||
function validateDependsOnArray(dependsOn) {
|
||||
if (!Array.isArray(dependsOn)) return;
|
||||
for (const dep of dependsOn) {
|
||||
if (typeof dep !== 'string' || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,100}$/.test(dep)) {
|
||||
throw new ValidationError(`Invalid dependency ID: ${String(dep)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dependencies route factory
|
||||
*
|
||||
@@ -124,10 +152,15 @@ module.exports = function({
|
||||
const { serviceId } = req.params;
|
||||
const { dependsOn } = req.body;
|
||||
|
||||
// Validate service ID and dependsOn entries before any state mutation
|
||||
validateServiceId(serviceId);
|
||||
|
||||
if (!Array.isArray(dependsOn)) {
|
||||
throw new ValidationError('Request body must include dependsOn as an array of service IDs');
|
||||
}
|
||||
|
||||
validateDependsOnArray(dependsOn);
|
||||
|
||||
// Validate first
|
||||
const validation = await dependencyManager.validateDependencies(serviceId, dependsOn);
|
||||
if (!validation.valid) {
|
||||
@@ -166,6 +199,8 @@ module.exports = function({
|
||||
router.delete('/:serviceId', asyncHandler(async (req, res) => {
|
||||
const { serviceId } = req.params;
|
||||
|
||||
validateServiceId(serviceId);
|
||||
|
||||
let found = false;
|
||||
await servicesStateManager.update(services => {
|
||||
const arr = Array.isArray(services) ? services : [];
|
||||
@@ -198,6 +233,9 @@ module.exports = function({
|
||||
router.post('/:serviceId/restart', asyncHandler(async (req, res) => {
|
||||
const { serviceId } = req.params;
|
||||
|
||||
// Validate service ID before any Docker or state operations
|
||||
validateServiceId(serviceId);
|
||||
|
||||
// Verify the service exists
|
||||
const services = await servicesStateManager.read();
|
||||
const allServices = Array.isArray(services) ? services : (services.services || []);
|
||||
|
||||
Reference in New Issue
Block a user