Skip to content

fix: [SNOW-3417049] refuse to follow symlinks that escape the project root#2984

Draft
sfc-gh-olorek wants to merge 1 commit into
mainfrom
proactive/SNOW-3417049-symlink-containment
Draft

fix: [SNOW-3417049] refuse to follow symlinks that escape the project root#2984
sfc-gh-olorek wants to merge 1 commit into
mainfrom
proactive/SNOW-3417049-symlink-containment

Conversation

@sfc-gh-olorek
Copy link
Copy Markdown
Contributor

@sfc-gh-olorek sfc-gh-olorek commented May 7, 2026

Pre-review checklist

  • I've confirmed that instructions included in README.md are still correct after my changes in the codebase.
  • I've added or updated automated unit tests to verify correctness of my new code.
  • I've added or updated integration tests to verify correctness of my new code.
  • I've confirmed that my changes are working by executing CLI's commands manually on MacOS.
  • I've confirmed that my changes are working by executing CLI's commands manually on Windows.
  • I've confirmed that my changes are up-to-date with the target branch.
  • I've described my changes in the release notes.
  • I've described my changes in the section below.
  • I've described my changes in the documentation.

Changes description

Jira: SNOW-3417049

BundleMap._absolute_src used resolve_without_follow (which is just os.path.abspath) to check that a source path stayed inside the project root. Because that helper never follows symlinks, a committed symlink whose lexical path was inside the project root but whose target pointed outside (for example project/data -> /etc) passed the containment check. The three os.walk(..., followlinks=True) call sites in the bundling pipeline would then traverse the real target and copy those files into the deploy root, from where snow app deploy uploads them to a Snowflake stage. A committed data -> /home/runner/.ssh symlink in any Native App repo is therefore enough to exfiltrate host secrets the next time someone runs snow app bundle / snow app deploy.

This PR hardens the check on both sides:

  • BundleMap._absolute_src now calls os.path.realpath on both the resolved source and the project root, and raises ArtifactError with a descriptive message if the real path escapes. Symlinks whose real target stays inside the project root continue to resolve normally — the fix only rejects escaping symlinks.
  • The two os.walk(..., followlinks=True) loops in BundleMap and _ArtifactPathMap prune any subdirectory or file whose real path escapes the project root, so a nested escaping symlink inside an otherwise-legitimate directory source (e.g. src/escape -> /etc inside a legitimate src/ mapping) is skipped rather than copied.

Unit tests cover three cases: a top-level symlink that escapes (must error), a nested symlink that escapes inside a legitimate directory source (must be pruned; siblings still bundled), and a legitimate symlink whose target remains inside the project root (must continue to be followed).

… root

`BundleMap._absolute_src` used `resolve_without_follow` (effectively
`os.path.abspath`) to check that a source path stayed inside the project
root. Because that helper never follows symlinks, a committed symlink
whose lexical path was inside the root but whose target pointed outside
(for example `project/data -> /etc`) passed the containment check. The
three `os.walk(..., followlinks=True)` sites in the bundling pipeline
would then traverse into the real target and copy those files into the
deploy root, from where `snow app deploy` uploads them to a Snowflake
stage — turning a repo checkout into an exfiltration vector for files
like `~/.aws/credentials` or `~/.ssh/`.

Harden the check on both sides:

* `_absolute_src` now calls `os.path.realpath` on both the source and
  the project root and raises `ArtifactError` if the real path escapes.
  Legitimate symlinks whose real target stays inside the project root
  continue to resolve normally.
* The two `os.walk(..., followlinks=True)` loops in `BundleMap` and
  `_ArtifactPathMap` prune any directory or file whose real path
  escapes the project root, so a nested escaping symlink inside a
  legitimate directory source is skipped instead of being copied.

Adds unit tests covering the top-level, nested, and legitimate-symlink
cases.
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.

1 participant