Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions src/api/abstract/abstract.router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,14 @@ const logger = new Logger('Validate');

const PROTECTED_INSTANCE_FIELDS = ['instanceName', 'instanceId'] as const;

function sanitizeUntrustedInput(source: Record<string, any> | undefined): Record<string, any> {
function sanitizeUntrustedInput(
source: Record<string, any> | undefined,
protectedFields: readonly string[] = PROTECTED_INSTANCE_FIELDS,
): Record<string, any> {
if (!source || typeof source !== 'object') return {};
const sanitized: Record<string, any> = {};
for (const [key, value] of Object.entries(source)) {
if ((PROTECTED_INSTANCE_FIELDS as readonly string[]).includes(key)) {
if (protectedFields.includes(key)) {
logger.warn(`Ignoring attempt to override protected field "${key}" via untrusted input`);
continue;
}
Expand All @@ -47,11 +50,16 @@ export abstract class RouterBroker {
const instance = request.params as unknown as InstanceDto;

if (request?.query && Object.keys(request.query).length > 0) {
Object.assign(instance, sanitizeUntrustedInput(request.query as Record<string, any>));
// /instance/fetchInstances has no ":instanceName" in its path either: it is a lookup route
// whose filter (instanceName or instanceId) legitimately arrives in the query string.
const queryProtectedFields = request.originalUrl.includes('/instance/fetchInstances') ? [] : undefined;
Object.assign(instance, sanitizeUntrustedInput(request.query as Record<string, any>, queryProtectedFields));
}

if (request.originalUrl.includes('/instance/create')) {
Object.assign(instance, sanitizeUntrustedInput(body));
// /instance/create has no ":instanceName" in its path, so the name can only come from the
// body and must be allowed through. instanceId stays protected: it is server-generated.
Object.assign(instance, sanitizeUntrustedInput(body, ['instanceId']));
}

Object.assign(ref, body);
Expand Down