Skip to content

Commit 69832ea

Browse files
Merge remote-tracking branch 'origin/main' into sammorrowdrums-raw-content-path-traversal-hardening
2 parents 65fe088 + 769340d commit 69832ea

4 files changed

Lines changed: 114 additions & 4 deletions

File tree

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1602,6 +1602,10 @@ docker run -i --rm \
16021602

16031603
Lockdown mode limits the content that the server will surface from public repositories. When enabled, the server checks whether the author of each item has push access to the repository. Private repositories are unaffected, and collaborators keep full access to their own content.
16041604

1605+
Lockdown mode is a best-effort content filter intended to reduce the risk of prompt injection from untrusted repository content (issues, pull requests, comments, commits, etc.). It is **not** an authorization boundary: it does not change what the underlying GitHub credential can read or write, and content withheld from a filtered tool response may still be reachable through other tools or direct GitHub API access with the same credential.
1606+
1607+
As an intentional exception, content authored by a small set of trusted bot accounts (currently `github-actions[bot]` and `copilot`) is always treated as safe, regardless of push access. This avoids filtering routine automation output (e.g. CI-generated commits or comments) that would otherwise be withheld under lockdown mode.
1608+
16051609
```bash
16061610
./github-mcp-server --lockdown-mode
16071611
```
@@ -1621,6 +1625,9 @@ Following tools will return an error when the author lacks the push access:
16211625

16221626
- `issue_read:get`
16231627
- `pull_request_read:get`
1628+
- `pull_request_read:get_diff`
1629+
- `pull_request_read:get_files`
1630+
- `pull_request_read:get_commits`
16241631

16251632
Following tools will filter out content from users lacking the push access:
16261633

docs/server-configuration.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,10 @@ When active, this mode will disable all tools that are not read-only even if the
292292

293293
Lockdown mode ensures the server only surfaces content in public repositories from users with push access to that repository. Private repositories are unaffected, and collaborators retain full access to their own content.
294294

295+
Lockdown mode is a best-effort content filter meant to reduce prompt-injection risk from untrusted repository content; it is not an authorization boundary. It does not restrict what the underlying credential can otherwise read or write, and content withheld from a filtered tool response may still be reachable through other tools or direct GitHub API access with the same credential.
296+
297+
As an intentional exception, content authored by trusted bot accounts (currently `github-actions[bot]` and `copilot`) is always treated as safe, regardless of push access, so routine automation output isn't filtered.
298+
295299
**Example:**
296300
<table>
297301
<tr><th>Remote Server</th><th>Local Server</th></tr>

pkg/github/pullrequests.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ Possible options:
132132
result, err := GetPullRequestFiles(ctx, client, deps, owner, repo, pullNumber, pagination)
133133
return attachIFC(result), nil, err
134134
case "get_commits":
135-
result, err := GetPullRequestCommits(ctx, client, owner, repo, pullNumber, pagination)
135+
result, err := GetPullRequestCommits(ctx, client, deps, owner, repo, pullNumber, pagination)
136136
return attachIFC(result), nil, err
137137
case "get_review_comments":
138138
gqlClient, err := deps.GetGQLClient(ctx)
@@ -412,7 +412,17 @@ func GetPullRequestFiles(ctx context.Context, client *github.Client, deps ToolDe
412412
return MarshalledTextResult(minimalFiles), nil
413413
}
414414

415-
func GetPullRequestCommits(ctx context.Context, client *github.Client, owner, repo string, pullNumber int, pagination PaginationParams) (*mcp.CallToolResult, error) {
415+
// GetPullRequestCommits returns the commits on a pull request. Commit messages
416+
// are user-authored content like the PR diff and files, so under lockdown mode
417+
// this applies the same PR-author check as GetPullRequestDiff/GetPullRequestFiles
418+
// rather than filtering individual commits: all commits on a pull request are
419+
// part of the same untrusted head branch, so a single check on the PR author is
420+
// sufficient and avoids an extra permission lookup per commit.
421+
func GetPullRequestCommits(ctx context.Context, client *github.Client, deps ToolDependencies, owner, repo string, pullNumber int, pagination PaginationParams) (*mcp.CallToolResult, error) {
422+
if restricted, err := enforcePullRequestLockdown(ctx, client, deps, owner, repo, pullNumber); restricted != nil || err != nil {
423+
return restricted, err
424+
}
425+
416426
opts := &github.ListOptions{
417427
PerPage: pagination.PerPage,
418428
Page: pagination.Page,

pkg/github/pullrequests_test.go

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1434,6 +1434,8 @@ func Test_GetPullRequestCommits(t *testing.T) {
14341434
expectError bool
14351435
expectedCommits []*github.RepositoryCommit
14361436
expectedErrMsg string
1437+
lockdownEnabled bool
1438+
restPermission string
14371439
}{
14381440
{
14391441
name: "successful commits fetch",
@@ -1497,16 +1499,103 @@ func Test_GetPullRequestCommits(t *testing.T) {
14971499
expectError: true,
14981500
expectedErrMsg: "failed to get pull request commits",
14991501
},
1502+
{
1503+
name: "lockdown enabled - author lacks push access",
1504+
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
1505+
GetReposPullsByOwnerByRepoByPullNumber: mockResponse(t, http.StatusOK, &github.PullRequest{
1506+
Number: github.Ptr(42),
1507+
User: &github.User{Login: github.Ptr("reader")},
1508+
}),
1509+
}),
1510+
requestArgs: map[string]any{
1511+
"method": "get_commits",
1512+
"owner": "owner",
1513+
"repo": "repo",
1514+
"pullNumber": float64(42),
1515+
},
1516+
lockdownEnabled: true,
1517+
restPermission: "read",
1518+
expectError: true,
1519+
expectedErrMsg: "access to pull request is restricted by lockdown mode",
1520+
},
1521+
{
1522+
name: "lockdown enabled - author has push access",
1523+
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
1524+
GetReposPullsByOwnerByRepoByPullNumber: mockResponse(t, http.StatusOK, &github.PullRequest{
1525+
Number: github.Ptr(42),
1526+
User: &github.User{Login: github.Ptr("writer")},
1527+
}),
1528+
GetReposPullsCommitsByOwnerByRepoByPullNumber: mockResponse(t, http.StatusOK, mockCommits),
1529+
}),
1530+
requestArgs: map[string]any{
1531+
"method": "get_commits",
1532+
"owner": "owner",
1533+
"repo": "repo",
1534+
"pullNumber": float64(42),
1535+
},
1536+
lockdownEnabled: true,
1537+
restPermission: "write",
1538+
expectError: false,
1539+
expectedCommits: mockCommits,
1540+
},
1541+
{
1542+
// Trusted bot logins (e.g. github-actions[bot], copilot) are treated as
1543+
// safe content sources regardless of push access, matching the
1544+
// intentional exception documented for lockdown mode.
1545+
name: "lockdown enabled - trusted bot author lacks push access",
1546+
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
1547+
GetReposPullsByOwnerByRepoByPullNumber: mockResponse(t, http.StatusOK, &github.PullRequest{
1548+
Number: github.Ptr(42),
1549+
User: &github.User{Login: github.Ptr("github-actions[bot]")},
1550+
}),
1551+
GetReposPullsCommitsByOwnerByRepoByPullNumber: mockResponse(t, http.StatusOK, mockCommits),
1552+
}),
1553+
requestArgs: map[string]any{
1554+
"method": "get_commits",
1555+
"owner": "owner",
1556+
"repo": "repo",
1557+
"pullNumber": float64(42),
1558+
},
1559+
lockdownEnabled: true,
1560+
restPermission: "read",
1561+
expectError: false,
1562+
expectedCommits: mockCommits,
1563+
},
1564+
{
1565+
name: "lockdown enabled - pull request fetch fails",
1566+
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
1567+
GetReposPullsByOwnerByRepoByPullNumber: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
1568+
w.WriteHeader(http.StatusNotFound)
1569+
_, _ = w.Write([]byte(`{"message": "Not Found"}`))
1570+
}),
1571+
}),
1572+
requestArgs: map[string]any{
1573+
"method": "get_commits",
1574+
"owner": "owner",
1575+
"repo": "repo",
1576+
"pullNumber": float64(999),
1577+
},
1578+
lockdownEnabled: true,
1579+
restPermission: "read",
1580+
expectError: true,
1581+
expectedErrMsg: "failed to get pull request",
1582+
},
15001583
}
15011584

15021585
for _, tc := range tests {
15031586
t.Run(tc.name, func(t *testing.T) {
15041587
client := mustNewGHClient(t, tc.mockedClient)
15051588
serverTool := PullRequestRead(translations.NullTranslationHelper)
1589+
1590+
var restClient *github.Client
1591+
if tc.lockdownEnabled {
1592+
restClient = mockRESTPermissionServer(t, tc.restPermission, nil)
1593+
}
1594+
15061595
deps := BaseDeps{
15071596
Client: client,
1508-
RepoAccessCache: stubRepoAccessCache(nil, 5*time.Minute),
1509-
Flags: stubFeatureFlags(map[string]bool{"lockdown-mode": false}),
1597+
RepoAccessCache: stubRepoAccessCache(restClient, 5*time.Minute),
1598+
Flags: stubFeatureFlags(map[string]bool{"lockdown-mode": tc.lockdownEnabled}),
15101599
}
15111600
handler := serverTool.Handler(deps)
15121601
request := createMCPRequest(tc.requestArgs)

0 commit comments

Comments
 (0)