Skip to content
Merged
Show file tree
Hide file tree
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
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,11 @@ export class WorkspaceReferenceResolverService {
}
const root = path.resolve(this.config.getOrThrow('WORKSPACE_ROOT'));
const resolved = path.resolve(root, rawPath);
if (resolved !== root && !resolved.startsWith(`${root}${path.sep}`)) {
// Same shape as `resolve()` above on purpose: a normalized path followed by ONE
// `startsWith(root + sep)` guard is what CodeQL models as containment; the
// compound `resolved !== root && …` form was not, and left js/path-injection open
// on every sink downstream. The root itself is not a satellite, so nothing is lost.
if (!resolved.startsWith(`${root}${path.sep}`)) {
throw new BadRequestException(
`${field} resolves outside the workspace root; send an opaque workspaceRef instead`,
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as path from 'path';
import { ICatalogLoader, IFileSystem } from '../../domain/interfaces';
import { IPlatformProviders } from '../ports/platform-detection.port';
import { InitProjectInput, InitProjectResult } from '../services/use-case.types';
Expand Down Expand Up @@ -27,9 +28,18 @@ export class InitializeProjectUseCase {
const artifacts: string[] = [];

try {
if (typeof input.name !== 'string' || !PROJECT_NAME.test(input.name)) {
const name = input.name;
// The regex already excludes separators and dot-segments; the two explicit
// checks restate the same fact in the form CodeQL models as a path sanitizer
// (no `..`, not absolute), so `${cwd}/${name}` reads as contained downstream.
if (
typeof name !== 'string' ||
!PROJECT_NAME.test(name) ||
name.includes('..') ||
path.isAbsolute(name)
) {
errors.push(
`Project name "${input.name}" is not a valid directory name: use letters, digits, ".", "-" or "_" (max 128 chars, cannot start with "." or "-")`,
`Project name "${name}" is not a valid directory name: use letters, digits, ".", "-" or "_" (max 128 chars, cannot start with "." or "-")`,
);
return { success: false, artifacts, warnings, errors };
}
Expand All @@ -55,7 +65,7 @@ export class InitializeProjectUseCase {
return { success: false, artifacts, warnings, errors };
}

const projectDir = `${cwd}/${input.name}`;
const projectDir = `${cwd}/${name}`;
await this.fs.ensureDir(projectDir);

await this.projectScaffolder.scaffoldEvolithYaml(input, projectDir);
Expand Down
20 changes: 15 additions & 5 deletions src/packages/mcp-server/src/tools/config.tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import * as path from 'node:path';
import * as fs from 'fs-extra';
import * as yaml from 'yaml';
import { McpTool } from '../mcp/tool.interface';
import { sanitizePathInput } from '../utils/path-security';

/**
* Segments that would walk the key path onto `Object.prototype` instead of into
Expand Down Expand Up @@ -60,13 +59,24 @@ export class ConfigToolService {
const keys = keySegments(key);
let target: Record<string, unknown> = config;
for (let i = 0; i < keys.length - 1; i++) {
const segment = keys[i];
// keySegments() already refused these; restated here, on the value that is
// written, because this literal comparison is the guard CodeQL models for
// js/prototype-pollution-utility — a Set lookup in another function is not.
if (segment === '__proto__' || segment === 'constructor' || segment === 'prototype') {
throw new Error(`Invalid key "${key}": "${segment}" is not an allowed segment`);
}
// Only descend into an OWN plain object; a scalar or an inherited property
// is replaced, never written through.
const next = Object.prototype.hasOwnProperty.call(target, keys[i]) ? target[keys[i]] : undefined;
if (typeof next !== 'object' || next === null || Array.isArray(next)) target[keys[i]] = {};
target = target[keys[i]] as Record<string, unknown>;
const next = Object.prototype.hasOwnProperty.call(target, segment) ? target[segment] : undefined;
if (typeof next !== 'object' || next === null || Array.isArray(next)) target[segment] = {};
target = target[segment] as Record<string, unknown>;
}
target[keys[keys.length - 1]] = value;
const leaf = keys[keys.length - 1];
if (leaf === '__proto__' || leaf === 'constructor' || leaf === 'prototype') {
throw new Error(`Invalid key "${key}": "${leaf}" is not an allowed segment`);
}
target[leaf] = value;
await fs.writeFile(configPath, yaml.stringify(config));
return { key, value, updated: true };
}
Expand Down
Loading