Skip to content

fix(backend): skip symlinks instead of refusing the whole repository - #449

Merged
parthrohit22 merged 1 commit into
devfrom
fix/447-skip-symlinks-instead-of-rejecting
Sep 11, 2026
Merged

parthrohit22 merged 1 commit into
devfrom
fix/447-skip-symlinks-instead-of-rejecting

Conversation

@parthrohit22

Copy link
Copy Markdown
Collaborator

Closes #447.

The problem

One symlink anywhere in a checkout rejected the entire import:

{"code": "validation_error",
 "message": "Repository contains a symlink, which is not supported.",
 "details": {"path": "/tests/certs/valid/ca"}}

That's psf/requests — a link in a TLS test fixture, and one of the most widely read Python repositories in existence could not be opened at all. Symlinks are ordinary: test fixtures, monorepo package links, a docs path pointing at a shared file.

The change

The parser records each link and steps over it.

The security posture strengthens rather than weakens. The original guard existed because is_dir(), is_file() and stat() all follow symlinks, so walking one would catalogue whatever it points at — including host filesystem content reached through a link escaping the checkout. Skipping never resolves the link at all, so that content is unreachable by construction rather than by a check. The link's own target is deliberately never resolved or reported.

It also brings the tree walk in line with what the same parser already does for metadata — _safe_file has always treated a symlinked manifest as "simply absent rather than failing the whole import". The tree walk was the only place that refused.

RepositoryMeta gains skippedSymlinks, so the omission is visible rather than silent and a reader can tell not followed from not present — the same honest-limits posture the review layer takes everywhere else.

Tests

The existing security tests are kept and made stricter. They previously asserted an exception was raised; they now assert the escape is unreachable — the link is absent from the tree, and nothing behind it appears anywhere in the response:

assert "/evil_link" not in paths
assert not any("secret.txt" in path for path in paths), "host content leaked through a symlink"
assert paths == ["/README.md"]
assert meta.skipped_symlinks == ["/evil_link"]

Plus new coverage for an internal link (not a second copy of its target), and for the max_file_count preflight walk (a skipped link counts for nothing against the budget).

Verified against the repository that motivated it

psf/requests, live import:

imported  : requests | 128 files | Python
skipped symlinks: ['/tests/certs/mtls/client/ca', '/tests/certs/valid/ca']
analysis  : completed
snapshot  : snap_6020973412af48aa8  sha256:470cc2dccc28a3c7eff

Contract

RepositoryMeta gained a field, so the frontend contract is regenerated and five test fixtures that build meta by hand are completed. Backend suite green, frontend 457/457, ruff and eslint clean.

A single symlink anywhere in a checkout rejected the entire import:

    Repository contains a symlink, which is not supported.
    {"path": "/tests/certs/valid/ca"}

That is psf/requests -- one link in a TLS test fixture, and one of the most
widely read Python repositories in existence could not be opened at all.
Symlinks are ordinary in real repositories: test fixtures, monorepo package
links, a docs path pointing at a shared file.

The parser now records each link and steps over it. Nothing about the
security posture weakens -- it strengthens. The original guard existed
because is_dir()/is_file()/stat() all follow symlinks, so walking one would
catalogue whatever it points at, including host filesystem content reached
through a link that escapes the checkout. Skipping never resolves the link at
all, so that content stays unreachable by construction rather than by a
check. The link's own target is deliberately never resolved or reported.

This also brings the file-tree walk in line with what the same parser already
does for metadata: _safe_file has always treated a symlinked manifest as
"simply absent rather than failing the whole import". The tree walk was the
only place that refused.

RepositoryMeta gains skipped_symlinks so the omission is visible rather than
silent -- a reader can tell "not followed" from "not present" -- which is the
same honest-limits posture the review layer takes for everything else it
cannot assess.

Verified against the repository that motivated this: psf/requests imports,
128 files, both links recorded, analysis completes and the snapshot seals.
@vercel

vercel Bot commented Sep 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
partha-frontend Ready Ready Preview Sep 11, 2026 4:42pm UTC

@parthrohit22
parthrohit22 merged commit 2191a43 into dev Sep 11, 2026
14 checks passed
@parthrohit22
parthrohit22 deleted the fix/447-skip-symlinks-instead-of-rejecting branch September 11, 2026 16:54
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.

fix(backend): a single symlink anywhere in a repository blocks import entirely

1 participant