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
12 changes: 12 additions & 0 deletions packages/cli/src/commands/adf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -480,7 +480,19 @@ function adfCreate(options: CLIOptions, args: string[]): number {

const modulePath = moduleArg.endsWith('.adf') ? moduleArg : `${moduleArg}.adf`;
const moduleRelPath = modulePath.replace(/\\/g, '/');

// Prevent directory traversal: reject paths that escape the .ai/ directory
if (moduleRelPath.includes('..') || path.isAbsolute(moduleRelPath)) {
throw new CLIError(`Invalid module path: "${moduleRelPath}". Path must not contain ".." or be absolute.`);
}

const moduleAbsPath = path.join(aiDir, moduleRelPath);
const resolvedAiDir = path.resolve(aiDir);
const resolvedModulePath = path.resolve(moduleAbsPath);
if (!resolvedModulePath.startsWith(resolvedAiDir + path.sep)) {
throw new CLIError(`Invalid module path: "${moduleRelPath}". Path must stay within ${aiDir}/.`);
}

fs.mkdirSync(path.dirname(moduleAbsPath), { recursive: true });

let fileCreated = false;
Expand Down
15 changes: 7 additions & 8 deletions packages/cli/src/git-helpers.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
/**
* Shared git invocation helpers.
*
* Centralizes all child-process git calls behind a single `runGit()` that
* uses `shell: true` for cross-platform PATH resolution (fixes WSL, CMD,
* PowerShell parity — see ADX-005 F2).
* Centralizes all child-process git calls behind a single `runGit()`.
* All args are hardcoded call-site strings, never user input — but we
* still avoid `shell: true` to eliminate any shell-injection surface.
*/

import { execFileSync } from 'node:child_process';
Expand All @@ -16,17 +16,16 @@ import type { GitCommit } from '@stackbilt/types';
/**
* Run a git command and return its stdout.
*
* Uses `shell: true` so that the OS shell resolves the `git` binary via
* PATH. This is the key cross-platform fix: `execFileSync` *without* a
* shell can fail on WSL/Windows when git lives in a PATH entry the Node
* process doesn't see directly.
* Does NOT use `shell: true` — Node resolves `git` via PATH directly, which
* works on WSL, Linux, macOS, and Windows (Git for Windows adds git to PATH
* at install time). Using shell: true is unnecessary here and would allow
* shell metacharacters in args to be interpreted as shell syntax.
*/
export function runGit(args: string[]): string {
return execFileSync('git', args, {
encoding: 'utf-8',
stdio: ['ignore', 'pipe', 'pipe'],
maxBuffer: 10 * 1024 * 1024,
shell: true,
});
}

Expand Down
Loading