Skip to content

Arbitrary command execution via crafted staged filenames in harness check #87

Description

@VikramNehreTR

Summary

harness check (cli pre-commit mode) is vulnerable to arbitrary shell command execution via a crafted staged filename. Confirmed live, not just from reading the code.

Root cause

src/cli/run.ts:

export function stagedContent(path: string): string {
  return execSync(`git show ":${path}"`, { encoding: "utf8" });
}

path comes from stagedFiles(), which is just git diff --cached --name-only --diff-filter=ACMR output — i.e. the literal names of staged files. Interpolating that into a shell string handed to execSync means any staged filename containing shell metacharacters runs as shell syntax.

Git's default core.quotepath=true does escape filenames containing an embedded " or \, which neutralizes the most obvious injection attempt (and is why my first try, using an embedded quote, didn't fire). But it does not escape backticks or $(...), so those go through untouched.

Reproduction (safe — uses touch, tested in a throwaway repo)

mkdir /tmp/harness-poc && cd /tmp/harness-poc && git init -q
npm i @fusengine/harness

FILENAME='x`touch pwned.txt`.ts'
echo "content" > "$FILENAME"
git add -- "$FILENAME"

npx harness check
# git then fails on the mangled remaining path ("x.ts" doesn't exist), but
# by that point the backtick command substitution has already run:
ls pwned.txt   # <- exists

Tested against 0.1.79 (latest as of 2026-07-20).

Impact

Anyone running harness check as a pre-commit hook (per the README's own .husky/pre-commit example) against a repo whose staged file list isn't fully trusted — e.g. right after checking out a contributor's branch/PR and running git add . — gets arbitrary command execution. This is exactly the scenario a pre-commit check is usually run in.

Suggested fix

Use execFileSync with an argv array instead of a shell string, so the filename is never parsed by a shell regardless of its content:

import { execFileSync } from "node:child_process";

export function stagedContent(path: string): string {
  return execFileSync("git", ["show", `:${path}`], { encoding: "utf8" });
}

Happy to send a PR for this if useful — just didn't want to open one unprompted before flagging the issue itself.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions