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
3 changes: 3 additions & 0 deletions src/scanner/walk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ export async function walkDirectory(
visited: Set<string> = new Set()
): Promise<DirectoryEntry[]> {
// Normalize and resolve path to handle symlinks
if (root.includes('..') || (root.startsWith('/'))) {
throw new Error(`Invalid directory path provided`);
}
Comment on lines +28 to +30

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The current security check is flawed and introduces a critical bug. The check root.startsWith('/') causes the recursive execution of walkDirectory to fail, because subdirectories are passed as absolute paths in recursive calls. This breaks the directory walking functionality beyond the first level.

Additionally, the check root.includes('..') is not robust. It can block valid paths (e.g., dir/../other_dir) and doesn't cover all path traversal cases.

A more robust and correct approach is to resolve the input path and ensure it stays within the current working directory. This check should only be performed on the initial call to avoid breaking recursion. We can detect the initial call by checking if the visited set is empty.

  if (visited.size === 0) {
    const resolvedPath = path.resolve(root);
    const workDir = path.resolve(process.cwd());
    if (!resolvedPath.startsWith(workDir)) {
      throw new Error('Invalid directory path provided: path is outside the current working directory.');
    }
  }

const normalizedRoot = path.resolve(root);

// Check for symlink loops
Expand Down
50 changes: 49 additions & 1 deletion tests/unit/scanner/walk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Graduated from story-32, story-43, and story-54
*/
import { describe, it, expect } from "vitest";
import { filterWorkItemDirectories, buildWorkItemList, normalizePath } from "@/scanner/walk";
import { filterWorkItemDirectories, buildWorkItemList, normalizePath, walkDirectory } from "@/scanner/walk";
import type { DirectoryEntry } from "@/types";

describe("filterWorkItemDirectories", () => {
Expand Down Expand Up @@ -159,3 +159,51 @@ describe("normalizePath", () => {
expect(normalized).toBe(unixPath);
});
});

describe("walkDirectory - Path Traversal Security", () => {
/**
* Security tests for path traversal vulnerability mitigation
* Tests various path traversal attack vectors
*/

it("GIVEN path with parent directory traversal WHEN walking THEN throws error", async () => {
// Given: Path with .. attempting to traverse up
const maliciousPath = "../../../etc/passwd";

// When/Then: Should reject the path
await expect(walkDirectory(maliciousPath)).rejects.toThrow("Invalid directory path provided");

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

With the suggested fix in src/scanner/walk.ts, the error message for path traversal attempts will change. Please update this assertion and the ones in the following tests (GIVEN path with embedded parent directory, GIVEN absolute path, GIVEN path with multiple parent traversals) to match the new, more descriptive error message.

Suggested change
await expect(walkDirectory(maliciousPath)).rejects.toThrow("Invalid directory path provided");
await expect(walkDirectory(maliciousPath)).rejects.toThrow('Invalid directory path provided: path is outside the current working directory.');

});

it("GIVEN path with embedded parent directory WHEN walking THEN throws error", async () => {
// Given: Path with .. in the middle
const maliciousPath = "specs/../../../etc/passwd";

// When/Then: Should reject the path
await expect(walkDirectory(maliciousPath)).rejects.toThrow("Invalid directory path provided");
});

it("GIVEN absolute path WHEN walking THEN throws error", async () => {
// Given: Absolute path starting with /
const absolutePath = "/etc/passwd";

// When/Then: Should reject absolute paths
await expect(walkDirectory(absolutePath)).rejects.toThrow("Invalid directory path provided");
});

it("GIVEN path with multiple parent traversals WHEN walking THEN throws error", async () => {
// Given: Path with multiple .. sequences
const maliciousPath = "../../../../../../etc/shadow";

// When/Then: Should reject the path
await expect(walkDirectory(maliciousPath)).rejects.toThrow("Invalid directory path provided");
});

it("GIVEN valid relative path WHEN walking THEN accepts path", async () => {
// Given: Valid relative path without traversal
const validPath = "specs/doing";

// When/Then: Should accept valid relative paths
// This will fail if directory doesn't exist, but won't fail the security check
await expect(walkDirectory(validPath)).rejects.toThrow(/Failed to walk directory/);
});
});