Skip to content
Open
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
4 changes: 4 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
## 2025-06-15 - Path Traversal in Export Logic
**Vulnerability:** A path traversal vulnerability existed in `src/export/gherkin.ts` where a user-provided `--output` path was directly joined with the project root and written to, without verifying if the path escaped the root directory (e.g., using `../../tmp/hacked.txt`).
**Learning:** `path.join(root, user_input)` is inherently unsafe against path traversal unless the resulting path is strictly validated to ensure it remains a child of `root`. The codebase handles arbitrary user paths in CLI flags.
**Prevention:** To prevent path traversal, construct absolute paths using `path.resolve(root, user_input)` and verify boundaries using `const rel = path.relative(root, target)`. Always check `rel === ".." || rel.startsWith("..${path.sep}") || path.isAbsolute(rel)` using `sep` from `node:path` to securely and accurately prevent traversal across all platforms. Use custom `VspecError` to provide a clean message to the CLI user on failure.
9 changes: 7 additions & 2 deletions src/export/gherkin.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { dirname, join, resolve, relative, isAbsolute, sep } from "node:path";
import { findUseCaseFile, readConfig, relativePath } from "../files.js";
import { parseUseCaseMarkdown } from "../format/parse.js";
import { VspecError } from "../errors.js";
import type { ParsedUseCase } from "../domain/types.js";

export function renderGherkin(useCase: ParsedUseCase): string {
Expand Down Expand Up @@ -39,7 +40,11 @@ export function exportGherkin(args: { key: string; output?: string; cwd?: string
if (!source) throw new Error("KEY_NOT_FOUND");
const text = renderGherkin(parseUseCaseMarkdown(readFileSync(source, "utf8")));
const output = args.output ?? join("tests", `${args.key}.feature`);
const outputPath = join(config.root, output);
const outputPath = resolve(config.root, output);
const rel = relative(config.root, outputPath);
if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
throw new VspecError("INVALID_ARGUMENT", `Output path "${output}" escapes the project root.`);
}
mkdirSync(dirname(outputPath), { recursive: true });
writeFileSync(outputPath, text);
return { key: args.key, text, path: relativePath(outputPath, config.root) };
Expand Down