Reject unsafe pathspecs
ober
2ed895f04b9c8073e2cedede3ac3e3fef057663b
--- a/README.md +++ b/README.md @@ -55,7 +55,8 @@ is retained as a label boundary only and does not create a separate verdict. By default, scans skip `vendor/`, `generated/`, `dist/`, `node_modules/`, and `.git/` paths. Use `--include PATH` or `--file PATH` to inspect one of those -paths explicitly. +paths explicitly. `--include` and `--file` accept repository-relative pathspecs; +absolute paths and `..` components are refused before invoking Git. Resource limits default to 500 changed files per commit, 20,000 added lines analyzed per commit, and 50,000 bytes per AI note. When a limit is hit, output --- a/main-binary.ss +++ b/main-binary.ss @@ -128,6 +128,21 @@ '())) (def (pathspec-args file) (if file (list "--" file) '())) +(def (parent-path-component? part) + (same-public-string? part "..")) + +(def (repo-relative-pathspec? path) + (and (string? path) + (not (string-empty? path)) + (not (path-absolute? path)) + (not (for/or ([part (string-split path #\/)]) + (parent-path-component? part))))) + +(def (validate-pathspec! path) + (if (and path (not (repo-relative-pathspec? path))) + (begin + (displayln (str "error: refusing path outside repository: " path)) + (exit 2)))) (def (range-spec from to) (cond [(and from to) (str from ".." to)] @@ -901,19 +916,21 @@ (let ([root (repo-root (options-path opts))]) (if (not root) (begin (displayln "error: path is not inside a Git repository") (exit 2)) - (let* ([commit (options-commit opts)] - [revs (commit-list root (options-count opts) commit (options-from opts) (options-to opts) (options-file opts))] - [findings (scan-repo root revs (options-file opts) (options-min-lines opts) - (options-metadata-only? opts) (options-heuristics-only? opts))]) - (cond [(string=? (options-command opts) "stats") (display-stats findings)] - [(string=? (options-command opts) "verify-authorship") (display-json root findings)] - [(string=? (options-command opts) "explain") - (if (pair? findings) - (if (string=? (options-format opts) "json") - (displayln (json-string (finding-json root (car findings)))) - (display-explain (car findings))) - (begin (displayln "error: commit not found") (exit 3)))] - [else (display-findings root findings (options-format opts))]))))) + (begin + (validate-pathspec! (options-file opts)) + (let* ([commit (options-commit opts)] + [revs (commit-list root (options-count opts) commit (options-from opts) (options-to opts) (options-file opts))] + [findings (scan-repo root revs (options-file opts) (options-min-lines opts) + (options-metadata-only? opts) (options-heuristics-only? opts))]) + (cond [(string=? (options-command opts) "stats") (display-stats findings)] + [(string=? (options-command opts) "verify-authorship") (display-json root findings)] + [(string=? (options-command opts) "explain") + (if (pair? findings) + (if (string=? (options-format opts) "json") + (displayln (json-string (finding-json root (car findings)))) + (display-explain (car findings))) + (begin (displayln "error: commit not found") (exit 3)))] + [else (display-findings root findings (options-format opts))])))))) (if (or (null? cli-args) (member "--help" cli-args) (member "-h" cli-args)) (usage) --- a/tests/fixture-smoke.sh +++ b/tests/fixture-smoke.sh @@ -214,6 +214,18 @@ git clone -q --depth 1 "file://$fixture" "$shallow_repo" shallow_json=$("$root/bin/jerboa-aigit" scan "$shallow_repo" --format json --count 1) printf '%s\n' "$shallow_json" | grep -q 'repository is shallow; parent history may be unavailable' +if "$root/bin/jerboa-aigit" scan "$fixture" --file ../outside >/tmp/jerboa-aigit-pathspec.out 2>&1; then + echo "parent-directory pathspec should fail" >&2 + exit 1 +fi +grep -q 'refusing path outside repository' /tmp/jerboa-aigit-pathspec.out + +if "$root/bin/jerboa-aigit" scan "$fixture" --include /tmp/outside >/tmp/jerboa-aigit-absolute-pathspec.out 2>&1; then + echo "absolute pathspec should fail" >&2 + exit 1 +fi +grep -q 'refusing path outside repository' /tmp/jerboa-aigit-absolute-pathspec.out + if "$root/bin/jerboa-aigit" scan "$fixture/nope" >/tmp/jerboa-aigit-invalid.out 2>&1; then echo "invalid repository path should fail" >&2 exit 1