mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-19 23:06:49 +00:00
fix: harden stack management operations (#1046)
This commit is contained in:
@@ -8,6 +8,7 @@ import { escapeCsvField } from '../utils/csv';
|
||||
import { getErrorMessage } from '../utils/errors';
|
||||
import { parseIntParam } from '../utils/parseIntParam';
|
||||
import { sanitizeForLog } from '../utils/safeLog';
|
||||
import { isValidStackName } from '../utils/validation';
|
||||
|
||||
const VALID_TARGET_TYPES = ['stack', 'fleet', 'system'] as const;
|
||||
const VALID_ACTIONS = ['restart', 'snapshot', 'prune', 'update', 'scan', 'auto_backup', 'auto_stop', 'auto_down', 'auto_start'] as const;
|
||||
@@ -37,6 +38,25 @@ function validateActionTarget(action: ScheduledAction, targetType: TargetType):
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateStackTarget(targetType: TargetType, targetId: unknown, nodeId: unknown): string | null {
|
||||
if (targetType !== 'stack') return null;
|
||||
|
||||
if (typeof targetId !== 'string' || !targetId.trim() || nodeId === null || nodeId === undefined) {
|
||||
return 'Stack operations require target_id and node_id.';
|
||||
}
|
||||
|
||||
if (targetId !== targetId.trim() || !isValidStackName(targetId)) {
|
||||
return 'Stack target_id must be a valid stack name.';
|
||||
}
|
||||
|
||||
const parsedNodeId = Number(nodeId);
|
||||
if (!Number.isInteger(parsedNodeId) || parsedNodeId <= 0) {
|
||||
return 'Stack operations require a valid node_id.';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateScanNode(nodeId: unknown): string | null {
|
||||
if (nodeId == null) return 'Scan action requires node_id.';
|
||||
const parsedNodeId = Number(nodeId);
|
||||
@@ -150,9 +170,8 @@ scheduledTasksRouter.post('/', (req: Request, res: Response): void => {
|
||||
if (action === 'update' && target_type === 'fleet' && !node_id) {
|
||||
res.status(400).json({ error: ERR_FLEET_NODE_REQUIRED }); return;
|
||||
}
|
||||
if (target_type === 'stack' && (!target_id || !node_id)) {
|
||||
res.status(400).json({ error: 'Stack operations require target_id and node_id.' }); return;
|
||||
}
|
||||
const stackTargetErr = validateStackTarget(target_type, target_id, node_id);
|
||||
if (stackTargetErr) { res.status(400).json({ error: stackTargetErr }); return; }
|
||||
|
||||
const optionalErr = validateOptionalFields(action, target_type, prune_targets, target_services, prune_label_filter);
|
||||
if (optionalErr) { res.status(400).json({ error: optionalErr }); return; }
|
||||
@@ -226,30 +245,33 @@ scheduledTasksRouter.put('/:id', (req: Request, res: Response): void => {
|
||||
|
||||
const { name, target_type, target_id, node_id, action, cron_expression, enabled, prune_targets, target_services, prune_label_filter, delete_after_run } = req.body;
|
||||
|
||||
if (target_type && !(VALID_TARGET_TYPES as readonly string[]).includes(target_type)) {
|
||||
if (target_type !== undefined && !(VALID_TARGET_TYPES as readonly string[]).includes(target_type)) {
|
||||
res.status(400).json({ error: 'Invalid target_type' }); return;
|
||||
}
|
||||
if (action && !(VALID_ACTIONS as readonly string[]).includes(action)) {
|
||||
if (action !== undefined && !(VALID_ACTIONS as readonly string[]).includes(action)) {
|
||||
res.status(400).json({ error: 'Invalid action' }); return;
|
||||
}
|
||||
|
||||
const finalAction = (action || existing.action) as ScheduledAction;
|
||||
const finalTargetType = (target_type || existing.target_type) as TargetType;
|
||||
const finalAction = (action ?? existing.action) as ScheduledAction;
|
||||
const finalTargetType = (target_type ?? existing.target_type) as TargetType;
|
||||
const finalTargetId = target_id !== undefined ? target_id : existing.target_id;
|
||||
const finalNodeId = node_id !== undefined ? node_id : existing.node_id;
|
||||
const targetErr = validateActionTarget(finalAction, finalTargetType);
|
||||
if (targetErr) { res.status(400).json({ error: targetErr }); return; }
|
||||
|
||||
if (finalAction === 'scan') {
|
||||
const finalNodeId = node_id !== undefined ? node_id : existing.node_id;
|
||||
const nodeErr = validateScanNode(finalNodeId);
|
||||
if (nodeErr) { res.status(400).json({ error: nodeErr }); return; }
|
||||
}
|
||||
if (finalAction === 'update' && finalTargetType === 'fleet') {
|
||||
const finalNodeId = node_id !== undefined ? node_id : existing.node_id;
|
||||
if (!finalNodeId) {
|
||||
res.status(400).json({ error: ERR_FLEET_NODE_REQUIRED }); return;
|
||||
}
|
||||
}
|
||||
|
||||
const stackTargetErr = validateStackTarget(finalTargetType, finalTargetId, finalNodeId);
|
||||
if (stackTargetErr) { res.status(400).json({ error: stackTargetErr }); return; }
|
||||
|
||||
const optionalErr = validateOptionalFields(finalAction, finalTargetType, prune_targets, target_services, prune_label_filter);
|
||||
if (optionalErr) { res.status(400).json({ error: optionalErr }); return; }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user