Skip to content

fix(action): block symlink escape past GITHUB_WORKSPACE - #5

Merged
esperanza-volkov merged 1 commit into
esperanza-volkov:mainfrom
anupamme:fix/action-symlink-escape
Sep 21, 2026
Merged

esperanza-volkov merged 1 commit into
esperanza-volkov:mainfrom
anupamme:fix/action-symlink-escape

Conversation

@anupamme

Copy link
Copy Markdown
Contributor

Context

Follow-up to #4, which was closed after discussion:

Conclusion of that thread:

  1. The CLI's readInput() should not reject relative .. paths — that's legitimate CLI usage (../staging/... etc.), and rejecting it would regress it.
  2. Literal .. traversal isn't reachable via the Action anyway — changedFiles() only ever passes git diff --name-only output to confdiff, and git refuses to track paths containing ...
  3. The real residual vector is scoped to the Action: runConfdiff(oldPath, f, …) reads the new side straight off the working tree at f. If a file tracked as a symlink has its target retargeted to point outside GITHUB_WORKSPACE (e.g. a repo that legitimately tracks a symlinked shared config, and a PR repoints it to something like /etc/passwd), reading f follows the symlink and the target's contents can end up in the sticky, public PR comment.

I verified the mechanics with real git repos: a plain file turning into a symlink between base/head is a git type-change (T), which the Action's existing --diff-filter=M already excludes. But a file already tracked as a symlink, with only its target changed in the PR, is reported as a normal M — and does reach runConfdiff. That's the exploitable path this PR closes.

Fix

action/index.mjs: before reading a changed file's new-side content, realpathSync(f) and skip (with a visible warning, same pattern as existing parse errors) any file whose resolved path isn't inside realpathSync(GITHUB_WORKSPACE).

No changes to src/cli.ts / readInput() — intentionally out of scope per the discussion above.

Test plan

  • npm test — all existing tests pass, plus two new cases in test/action-symlink.test.ts:
    • a tracked symlink retargeted outside the workspace is skipped and its target's content never appears in output
    • a normal in-workspace config change still produces the expected semantic diff (no regression)

🤖 Generated with Claude Code

runConfdiff() reads the new side of a changed file straight off the
working tree, which follows symlinks. A PR that retargets an
already-tracked symlink to point outside the checkout (e.g. a repo
that legitimately symlinks shared config) could have its target's
contents read and posted in the sticky PR comment.

Canonicalize each changed file's path with realpathSync and skip
(with a visible warning, same pattern as parse errors) any file that
resolves outside GITHUB_WORKSPACE.

This replaces the CLI-side "..".-segment check proposed in PR esperanza-volkov#4,
per discussion there: the CLI must keep supporting legitimate
relative paths, git never tracks ".."-containing paths so that check
was dead code on the Action's actual input, and the real residual
vector is this Action-side symlink read.

Refs: esperanza-volkov#4 (comment)
      esperanza-volkov#4 (comment)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@esperanza-volkov esperanza-volkov left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Reviewed and reproduced end-to-end. Confirmed the residual vector is real for lenient formats: with a config tracked as a symlink at base (e.g. config.csv), git show base: returns the link text which a strict parser (JSON) rejects — but CSV/.env parse it fine, so retargeting the symlink to an out-of-workspace file (/tmp/secret.csv) leaked its contents straight into the sticky PR comment. Your realpathSync(f) containment check (skip-with-warning, using workspace + sep to avoid the /foo vs /foobar prefix trap) blocks it cleanly, and the two new tests plus the full suite (123/123) confirm no regression for normal in-workspace changes. Correctly scoped to the Action boundary with src/cli.ts untouched, exactly as discussed. Nicely done — thank you.

@esperanza-volkov
esperanza-volkov merged commit 76226f4 into esperanza-volkov:main Sep 21, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants