#2573·checkout

`path` checkout follows a repository-controlled symlink outside `GITHUB_WORKSPACE`

Author: N0zoM1z0Created Sep 12, 2026Updated Sep 12, 2026

Summary

actions/checkout v7.0.1 validates the requested checkout path lexically, but does not validate the physical path after existing symlinks are resolved. A repository checked out first can therefore control an intermediate component of a later checkout destination.

Minimal workflow shape:

yaml
- uses: actions/checkout@v7
  # This repository contains: private -> ../outside
- uses: actions/checkout@v7
  with:
    repository: trusted-org/trusted-repository
    path: private

The second path is accepted because workspace/private is lexically below $GITHUB_WORKSPACE. The checkout implementation then follows the symlink. With clean: true, it can remove entries in the physical target, initialize a Git repository there, and materialize the trusted repository outside the declared workspace.

The same primitive also reaches protected metadata: a repository entry metadata -> .git lets a later path: metadata checkout remove the original checkout's .git contents and create a nested repository below it.

Affected source

The report is based on actions/checkout v7.0.1, source commit f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a.

The exact source locations are:

There is no realpath check between the lexical guard and the cleanup/Git operations.

Reproduction

The attached reproducer uses the real getSource() implementation. It creates three local repositories: a malicious repository containing the symlinks, a trusted repository, and a regular-directory control.

From the directory containing this report:

bash
git clone https://github.com/actions/checkout.git attachments/vendor/actions-checkout
git -C attachments/vendor/actions-checkout checkout --detach f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a
(cd attachments/vendor/actions-checkout && npm ci --ignore-scripts && npm run build)

ACTION_CHECKOUT_DIR=attachments/vendor/actions-checkout \
  attachments/reproduce.sh attachments/replay

The script refuses to overwrite an existing output directory. It requires Node.js, Git, and the pinned action checkout. The source tree is only used to build the action; all Git operations in the test are performed by the action's own source provider and a native Git binary.

See attachments/evidence.md and attachments/run.log. The log contains only redacted relative fixture names and no credentials.

Expected behavior

Every checkout destination should remain physically below the workspace root, and cleanup should cover every filesystem effect of the checkout. A path that contains a repository-controlled symlink should either be rejected or be resolved and checked against the workspace and protected metadata boundaries before cleanup, initialization, fetch, or checkout begins.

Observed behavior

The control checkout resolves to an ordinary directory below the workspace. The attack checkout reports success but produces the following independent filesystem results:

control_inside=true
requested=workspace/private
resolved=outside
preexisting_file_removed=true
outside_repository_survives=true
metadata_sentinel_removed=true
main_git_config_removed=true
nested_repository_published=true
content=trusted-checkout-content

The external repository remains after the workspace symlink is removed. This means a cleanup rooted at $GITHUB_WORKSPACE does not reclaim the materialized trusted repository.

Security impact and prerequisites

The direct impact is a physical-root and cleanup-boundary violation:

  • an untrusted repository controls the destination of a later trusted checkout;
  • the later checkout can delete a pre-existing file at the physical target;
  • trusted source and .git metadata can be written outside the declared checkout directory; and
  • workspace-rooted cleanup can leave the materialization behind.

This matters for self-hosted runners and containerized jobs that treat $GITHUB_WORKSPACE as a writable, disposable boundary. The sequence requires the first repository to be materialized before the later checkout uses the colliding nested path, and the physical target must be writable by the runner.

This report does not claim host compromise or standalone arbitrary code execution. The demonstrated security property is confinement and cleanup of a trusted checkout in a multi-checkout workflow.

Related history and novelty

The closest public records are not the same behavior:

  • #1812 requests support for paths outside $GITHUB_WORKSPACE; it does not describe a symlink bypass of the documented boundary.
  • #2393 concerns a symlinked runner _work directory and credential includeIf matching. It does not involve a repository-controlled intermediate component or a later checkout redirected outside the workspace.

I did not find an actions/checkout issue or advisory describing this intermediate-symlink/later-checkout sequence. The attached run is therefore a new, source-realized report against the pinned release, subject to the maintainers' interpretation of the workspace boundary for self-hosted runners.

Suggested remediation

Resolve the candidate destination with realpath-style semantics after accounting for existing components, then enforce that the canonical path is inside the canonical workspace and outside protected runner state. Apply the same check immediately before cleanup and before passing the path to Git. Add regression tests for:

  1. a committed intermediate symlink to a sibling directory;
  2. a committed intermediate symlink to .git;
  3. cleanup after the checkout; and
  4. nested trusted checkouts following an untrusted checkout.

Attachments

attachments.zip