Repository navigation
Fix read-tool workspace permission scoping regression (daily workflows killed by denial threshold) #49840
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix read-tool workspace permission scoping regression (daily workflows killed by denial threshold) #49840
Changes from all commits
0b738c5
24ba62d
946ded1
19fa211
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -352,6 +352,21 @@ function buildCopilotSDKPermissionHandler(permissionConfig, approveAll, logOptio | |
| return allowedToolEntries.has("write"); | ||
| case "read": | ||
| // Any read grant (read, read(...), read:*) is path-agnostic in Copilot SDK. | ||
| // Always allow reads for paths at or under the workspace root (GITHUB_WORKSPACE). | ||
| // Every workflow runs inside its own checkout and must be able to read its source tree | ||
| // regardless of any narrower tool-permission scoping configured elsewhere. | ||
| // | ||
| // Use path.resolve + path.relative for containment rather than a string-prefix check so | ||
| // that ".." traversal paths (e.g. workspace/../../../../etc/passwd) are rejected and | ||
| // relative paths (e.g. "AGENTS.md") are correctly resolved inside the workspace. | ||
| if (logOptions?.workspaceRoot && typeof request.path === "string" && request.path.length > 0) { | ||
| const resolvedWorkspace = path.resolve(logOptions.workspaceRoot); | ||
| const resolvedPath = path.isAbsolute(request.path) ? path.resolve(request.path) : path.resolve(resolvedWorkspace, request.path); | ||
| const rel = path.relative(resolvedWorkspace, resolvedPath); | ||
| if (rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel))) { | ||
| return true; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Security: path-traversal bypass — workspace containment check can be defeated with
// Proof
normalizePermissionPath("/home/runner/work/gh-aw/gh-aw/../../../etc/passwd")
// → "/home/runner/work/gh-aw/gh-aw/../../../etc/passwd"
// .startsWith("/home/runner/work/gh-aw/gh-aw/") === true ← incorrectly approvedFix: resolve both paths with const { posix } = require("path");
// inside case "read":
if (logOptions?.workspaceRoot && typeof request.path === "string" && request.path.length > 0) {
const normalizedWorkspace = posix.normalize(normalizePermissionPath(logOptions.workspaceRoot));
const normalizedPath = posix.normalize(normalizePermissionPath(request.path));
if (normalizedPath === normalizedWorkspace || normalizedPath.startsWith(normalizedWorkspace + "/")) {
return true;
}
}Add a corresponding test case: expect(onPermissionRequest({ kind: "read", path: "/home/runner/work/gh-aw/gh-aw/../../../etc/passwd", intention: "" }))
.toEqual({ kind: "reject", feedback: "Tool invocation is not allowed by workflow tool permissions." });@copilot please address this. |
||
| } | ||
| } | ||
| return hasReadGrant || allowedToolEntries.has("shell") || isReadPathAllowedByShellRules(request.path, readablePathPatterns, logOptions?.workspaceRoot); | ||
| case "url": | ||
| return allowedToolEntries.has("web_fetch"); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[/tdd] The regression test for #49836 does not test the case where
GITHUB_WORKSPACEis unset (i.e.,logOptions.workspaceRootisundefined). WhenworkspaceRootis absent, the new early-return is skipped entirely — meaning the pre-existing fallback still applies. A test confirming "whenGITHUB_WORKSPACEis not set, behaviour is unchanged" would prevent future regressions where someone accidentally makes the workspace check mandatory.💡 Suggested additional assertion
This pins the contract: the early-return is only active when
workspaceRootis provided.@copilot please address this.