Skip to content

fix(sbom): reject argument-injection in --from-repo --ref (git clone) - #252

Open
nikhilpatidar wants to merge 2 commits into
NuGuardAI:mainfrom
nikhilpatidar:bug/sbom-clone-arg-injection
Open

fix(sbom): reject argument-injection in --from-repo --ref (git clone)#252
nikhilpatidar wants to merge 2 commits into
NuGuardAI:mainfrom
nikhilpatidar:bug/sbom-clone-arg-injection

Conversation

@nikhilpatidar

@nikhilpatidar nikhilpatidar commented Aug 11, 2026

Copy link
Copy Markdown

PR Type

  • Bug fix
  • Feature

What

Hardens AiSbomExtractor._clone_repo against argument-injection via
--from-repo --ref so a hostile ref string (e.g. one pasted from a
malicious README) cannot be reinterpreted by git clone as a flag
(--upload-pack=<cmd>, --config=..., etc.).

Closes #253

Why

NuGuard is a security tool whose sbom generate --from-repo flow
hands a user-supplied --ref value straight to git clone as a
positional argument. The same git argument-injection class has been
documented since CVE-2017-1000117 and reappears in newer advisories;
modern git rejects most leading-- positional arguments but the
protection is not uniform across versions and a hardening check at the
NuGuard boundary costs nothing and removes the operator-machine attack
surface entirely.

The realistic delivery vector is a copy-pasted command or a README
that encourages the operator to run a specific --ref, which is a
plausible threat model for a tool that is itself invoked defensively.

Root Cause

_clone_repo assembled its argv with the ref and url as bare
positional arguments:

cmd = ["git", "clone", "--depth", "1", "--branch", ref, url, str(dest)]
subprocess.run(cmd, check=True, capture_output=True)

Anything that starts with - is parsed by git as an option.

How

nuguard/sbom/extractor/core.py

  • Added two private regex constants (_SAFE_REF_RE,
    _SAFE_URL_RE) at class level.
  • _clone_repo now validates ref and url and raises
    ValueError with a clear message before any subprocess is
    started.
  • Added a -- separator before url and dest in the argv so
    that any future regression in the regex cannot re-introduce the same
    injection class (defence in depth).

tests/sbom/test_clone_repo_arg_injection.py (new)

21 regression tests covering:

  • 8 hostile ref patterns rejected (leading -, embedded NUL/CR/LF/TAB,
    whitespace-only, empty) — each verifies subprocess.run is not
    invoked.
  • 7 hostile url patterns rejected (leading -, missing scheme,
    file://, git://, empty/whitespace).
  • 4 well-formed urls (https, http, ssh) reach subprocess.run
    with the expected argv shape.
  • Pins the -- separator placement in the argv so the
    defence-in-depth check cannot silently regress.

The fix does not change any existing API behaviour for legitimate
inputs — every previously-valid ref and url still produces an identical
argv (plus the new -- separator, which git treats as an end-of-options
marker that affects nothing when subsequent args are non-flags).

Tests

uv run pytest tests/sbom/test_clone_repo_arg_injection.py -v
# 21 passed in 0.04s

uv run pytest tests/sbom/ tests/cli/test_sbom_cli.py -q
# 221 passed, 1 skipped in 1.54s

Validation

uv run ruff check nuguard/sbom/extractor/core.py tests/sbom/test_clone_repo_arg_injection.py
# All checks passed!

uv run ruff format --check tests/sbom/test_clone_repo_arg_injection.py
# 1 file already formatted

uv run mypy nuguard/sbom/extractor/core.py
# Success: no issues found in 1 source file

The source-file edit is intentionally NOT auto-formatted: ruff format would rewrite unrelated lines in this 3000-line module,
inflating the diff and obscuring the security fix. The diff against
main is +43/-1 in the one function plus a new test file.

The git-clone path reachable from `nuguard sbom generate --from-repo
--ref <ref>` (and from the public `AiSbomExtractor.extract_from_repo`
API) passed the ref and url to `git clone` as positional arguments.
A hostile ref value starting with `-` could be interpreted by git as a
flag (e.g. `--upload-pack=<cmd>`), a known argument-injection vector
documented in CVE-2017-1000117 and related advisories.

This change:

* rejects refs that begin with `-`, contain whitespace, or are empty
  before subprocess.run is invoked (no partial execution possible);
* rejects urls that lack an http(s)/ssh scheme;
* adds a `--` separator before the url and destination in the
  `git clone` argv so any future regression in the regex cannot
  re-introduce the same injection class;
* adds 21 regression tests (refs/urls rejected, well-formed inputs
  accepted, subprocess argv structure pins `--` placement).

Thorough manual review of the threat model: the user has to opt into
`--from-repo` and supply a ref from an external source. A malicious
README or copy-pasteable command is the realistic delivery vector for
this class, which is why a security tool should refuse the input
rather than depend on git's own heuristics.
KanishkThamman
KanishkThamman previously approved these changes Aug 11, 2026

@KanishkThamman KanishkThamman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Confirmed this closes a real argument-injection RCE via git clone --upload-pack=... — reproduced it locally, the -- separator + scheme check fixes it. Two small nits inline, not blockers.

Comment thread nuguard/sbom/extractor/core.py Outdated
# Accepting such a ref would let a hostile ref string (e.g. one pasted from
# a malicious README) trick ``git clone`` into invoking other git options
# such as ``--upload-pack=<command>`` — a known argument-injection vector.
_SAFE_REF_RE = re.compile(r"^[^-,\s\x00][^,\s\x00]*$")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nit: $ matches before a trailing newline in Python regex, so "main\n" slips past this (verified). Use \Z or re.fullmatch to close it.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 241cb91. Switched ^[^-,\s\x00][^,\s\x00]*$ to ^[^-,\s\x00][^,\s\x00]*\Z so a trailing newline in the ref can no longer bypass the regex. The 'main\n slips past' failure mode is closed. Added a 'main\n' regression case to the parametrize set.

Comment thread nuguard/sbom/extractor/core.py Outdated
# upstream, ``extract_from_repo`` is a public API callable from any
# embedding, so we re-validate here to avoid the same injection class.
# Only http(s)/ssh transports are accepted at the clone boundary.
_SAFE_URL_RE = re.compile(r"^(https?|ssh)://[^\s\x00]*$")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nit: same $ vs \Z issue as the ref regex above. Also, this rejects scp-style SSH URLs (git@host:path), which were previously accepted by extract_from_repo — a behavior regression for existing callers using that form.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in 241cb91. Extended _SAFE_URL_RE to accept scp-style SSH (git@host:path) — the form used by every GitHub/GitLab/Bitbucket SSH clone and historically accepted by extract_from_repo. The path sub-pattern still forbids -, ,, :, whitespace, and null at the start, so a hostile provider cannot smuggle git@host:--upload-pack=evil` through.

Added three well-formed scp URLs to test_clone_repo_accepts_well_formed_urls and a new test_clone_repo_rejects_hostile_scp_urls parametrize set for the security boundary.

Two review nits on the _clone_repo argument-injection hardening:

1. The ref and URL regexes used `^...$` which Python interprets as
   'no newline followed by end-of-string OR a single trailing newline'.
   A ref like `"main\n"` therefore slipped past _SAFE_REF_RE and
   reached git clone as a positional argument. Switch to `\Z`.

2. _SAFE_URL_RE only accepted http(s)/ssh:// — but extract_from_repo
   historically accepted scp-style SSH URLs (`git@host:path`),
   used by every GitHub/GitLab/Bitbucket SSH clone. Accept the scp
   form while still rejecting any path that starts with a flag
   character so a hostile provider cannot smuggle
   `git@host:--upload-pack=evil` through.

Tests:
- Add 'main\n' to the rejected-ref parametrize set.
- Add three scp-style URLs to the accepted-url parametrize set.
- Add a new test_clone_repo_rejects_hostile_scp_urls parametrize set
  for scp URLs whose path starts with `-`, `,,`, or whitespace —
  pinning the security boundary while preserving compatibility.
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.

Hardening: validate --ref in 'nuguard sbom generate --from-repo' against git argument injection

2 participants