feat(mcp,gitcheck): block server-side commits that bypass signing#30
feat(mcp,gitcheck): block server-side commits that bypass signing#30
Conversation
GitHub API commits (Contents API, Git Data API) bypass local git signing config, producing unsigned commits. Block both vectors: - MCP: deny push_files + create_or_update_file (was Allow, now Deny) - Bash: block gh api calls to contents/, git/commits, git/trees, git/refs, git/blobs endpoints (new FR-GS-9 pattern) Also updates test counts and doc comments for the new pattern. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Greptile SummaryThis PR adds FR-GS-9, a new git-safety rule that blocks two surfaces through which the GitHub API can create server-side commits that bypass local GPG/SSH signing: the MCP layer ( Key changes:
Confidence Score: 3/5
Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Bash command] --> B{RE_GH_API_COMMIT_PATH\nmatches?}
B -- No --> Z[GitResult::Ok]
B -- Yes --> C{RE_GH_API_WRITE_METHOD\nmatches?\n-X/-Xpost/--method=POST etc.}
C -- Yes --> BLOCK[GitResult::Block\nBLOCKED: bypasses commit signing]
C -- No --> D{RE_GH_API_WRITE_BODY\nmatches?\n-f/--field/-F/--data/--input + sep}
D -- Yes --> BLOCK
D -- No --> Z
subgraph GAP [Known gap]
E["'-fvalue' no-separator form\nnot matched by RE_GH_API_WRITE_BODY"]
end
D -. false negative .-> GAP
Last reviewed commit: "fix(gitcheck): handl..." |
The gh api commit endpoint regex was matching read-only GETs to contents/ and git/ endpoints. Now requires an explicit write method flag (-X/-\-method PUT/POST/PATCH/DELETE) before blocking. Adds test cases for read-only access to contents/, git/refs, git/trees, git/blobs, and git/commits endpoints. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
@greptile review |
The changelog binary may exit before reading all of stdin when given empty input. On Linux CI this consistently causes EPIPE on the write. Ignore the write error since early exit is expected behavior. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
@greptile review |
The method flag (-X/--method) can appear before or after the path in gh api calls. Split into two independent regexes (path match + write method match) so flag ordering doesn't matter. Adds test cases for method-flag-before-path variants. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
@greptile review |
- FR-GS-9: extend write method regex to match equals-delimited form (--method=POST) in addition to space-delimited (--method POST) - MCP: add delete_file to the server-side commit deny list (DELETE on Contents API also creates a commit) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
@greptile review |
pflag accepts -X with the value directly concatenated (-XPOST) with no separator. Split the regex into separate short/long flag patterns: -X allows zero separators, --method requires at least one. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
@greptile review |
gh api auto-promotes to POST when body fields (-f/-F/-d/--input) are present without an explicit method flag. Add RE_GH_API_WRITE_BODY regex and OR it into the FR-GS-9 check. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
@greptile review |
Long body flags (--field, --data, --input, etc.) accept =value syntax. Extend RE_GH_API_WRITE_BODY to match [=\s] after flag names. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
| static RE_GH_API_WRITE_BODY: LazyLock<Regex> = LazyLock::new(|| { | ||
| Regex::new(r"\s(-f|--field|-F|--raw-field|-d|--data|--input)[=\s]").unwrap() | ||
| }); |
There was a problem hiding this comment.
Short-flag no-separator form bypasses
RE_GH_API_WRITE_BODY
RE_GH_API_WRITE_BODY requires at least one = or whitespace character after the flag name ([=\s]). However, cobra/pflag — which gh uses — also accepts values concatenated directly against a short flag with no separator. The pattern -fvalue is equivalent to -f value for string flags.
This means a command like gh api repos/owner/repo/contents/file.txt -fmessage=create passes the guard:
RE_GH_API_COMMIT_PATHmatches (contents/)RE_GH_API_WRITE_METHODdoes not match (no-X/--method)RE_GH_API_WRITE_BODY:\s(-f)[=\s]— the character immediately after-fism, not=or whitespace → no match
Result: GitResult::Ok — bypassed.
The same applies to -F and -d short flags. This is the exact same class of bug that was fixed in RE_GH_API_WRITE_METHOD via [=\s]* (zero-or-more). A parallel fix would make the trailing separator optional for the short-flag forms:
| static RE_GH_API_WRITE_BODY: LazyLock<Regex> = LazyLock::new(|| { | |
| Regex::new(r"\s(-f|--field|-F|--raw-field|-d|--data|--input)[=\s]").unwrap() | |
| }); | |
| static RE_GH_API_WRITE_BODY: LazyLock<Regex> = LazyLock::new(|| { | |
| Regex::new(r"\s(-f|-F|-d)[=\s]?|\s(--field|--raw-field|--data|--input)[=\s]").unwrap() | |
| }); |
The test suite should also add coverage for the no-separator form:
// Short-flag concat (no separator) — implicit POST
"gh api repos/owner/repo/contents/file.txt -fmessage=create",
"gh api repos/owner/repo/git/commits -Fmessage=bypass",
Summary
push_filesandcreate_or_update_filemoved from Allow → Deny in GitHub MCP routing — these use the GitHub Contents API to create server-side commits that bypass local GPG/SSH signinggh apicalls to commit-creating endpoints (contents/,git/commits,git/trees,git/refs,git/blobs)Test plan
test_github_server_side_commit_deny— verifies both MCP tools return Denytest_gh_api_commit_endpoints_blocked— verifies all 5 gh api patterns blockedtest_gh_api_read_endpoints_not_blocked— verifies read-only gh api calls still passcargo test)🤖 Generated with Claude Code