fix(core): secure argsPattern and revert WEB_FETCH_TOOL_NAME escalation (#22104)

Co-authored-by: Taylor Mullen <ntaylormullen@google.com>
This commit is contained in:
Spencer
2026-03-11 22:26:21 -04:00
committed by GitHub
parent 35bf746e62
commit f090736ebc
8 changed files with 67 additions and 26 deletions
+1 -1
View File
@@ -98,7 +98,7 @@ toolName = ["write_file", "replace"]
decision = "allow"
priority = 70
modes = ["plan"]
argsPattern = "\"file_path\":\"[^\"]+[\\\\/]+\\.gemini[\\\\/]+tmp[\\\\/]+[\\w-]+[\\\\/]+[\\w-]+[\\\\/]+plans[\\\\/]+[\\w-]+\\.md\""
argsPattern = "\\x00\"file_path\":\"[^\"]+[\\\\/]+\\.gemini[\\\\/]+tmp[\\\\/]+[\\w-]+[\\\\/]+[\\w-]+[\\\\/]+plans[\\\\/]+[\\w-]+\\.md\"\\x00"
# Explicitly Deny other write operations in Plan mode with a clear message.
[[rule]]
+22 -5
View File
@@ -57,7 +57,11 @@
* // Returns: '{"safe":"data"}'
*/
export function stableStringify(obj: unknown): string {
const stringify = (currentObj: unknown, ancestors: Set<unknown>): string => {
const stringify = (
currentObj: unknown,
ancestors: Set<unknown>,
isTopLevel = false,
): string => {
// Handle primitives and null
if (currentObj === undefined) {
return 'null'; // undefined in arrays becomes null in JSON
@@ -89,7 +93,10 @@ export function stableStringify(obj: unknown): string {
if (jsonValue === null) {
return 'null';
}
return stringify(jsonValue, ancestors);
// The result of toJSON is effectively a new object graph, but it
// takes the place of the current node, so we preserve the top-level
// status of the current node.
return stringify(jsonValue, ancestors, isTopLevel);
} catch {
// If toJSON throws, treat as a regular object
}
@@ -101,7 +108,7 @@ export function stableStringify(obj: unknown): string {
if (item === undefined || typeof item === 'function') {
return 'null';
}
return stringify(item, ancestors);
return stringify(item, ancestors, false);
});
return '[' + items.join(',') + ']';
}
@@ -115,7 +122,17 @@ export function stableStringify(obj: unknown): string {
const value = (currentObj as Record<string, unknown>)[key];
// Skip undefined and function values in objects (per JSON spec)
if (value !== undefined && typeof value !== 'function') {
pairs.push(JSON.stringify(key) + ':' + stringify(value, ancestors));
let pairStr =
JSON.stringify(key) + ':' + stringify(value, ancestors, false);
if (isTopLevel) {
// We use a null byte (\0) to denote structural boundaries.
// This is safe because any literal \0 in the user's data will
// be escaped by JSON.stringify into "\u0000" before reaching here.
pairStr = '\0' + pairStr + '\0';
}
pairs.push(pairStr);
}
}
@@ -125,5 +142,5 @@ export function stableStringify(obj: unknown): string {
}
};
return stringify(obj, new Set());
return stringify(obj, new Set(), true);
}
+32 -8
View File
@@ -89,6 +89,25 @@ export function buildArgsPatterns(
return [argsPattern];
}
/**
* Builds a regex pattern to match a specific parameter and value in tool arguments.
* This is used to narrow tool approvals to specific parameters.
*
* @param paramName The name of the parameter.
* @param value The value to match.
* @returns A regex string that matches "<paramName>":<value> in a JSON string.
*/
export function buildParamArgsPattern(
paramName: string,
value: unknown,
): string {
const encodedValue = JSON.stringify(value);
// We wrap the JSON string in escapeRegex and prepend/append \\0 to explicitly
// match top-level JSON properties generated by stableStringify, preventing
// argument injection bypass attacks.
return `\\\\0${escapeRegex(`"${paramName}":${encodedValue}`)}\\\\0`;
}
/**
* Builds a regex pattern to match a specific file path in tool arguments.
* This is used to narrow tool approvals for edit tools to specific files.
@@ -97,11 +116,18 @@ export function buildArgsPatterns(
* @returns A regex string that matches "file_path":"<path>" in a JSON string.
*/
export function buildFilePathArgsPattern(filePath: string): string {
const encodedPath = JSON.stringify(filePath);
// We must wrap the JSON string in escapeRegex to ensure regex control characters
// (like '.' in file extensions) are treated as literals, preventing overly broad
// matches (e.g. 'foo.ts' matching 'fooXts').
return escapeRegex(`"file_path":${encodedPath}`);
return buildParamArgsPattern('file_path', filePath);
}
/**
* Builds a regex pattern to match a specific directory path in tool arguments.
* This is used to narrow tool approvals for list_directory tool.
*
* @param dirPath The path to the directory.
* @returns A regex string that matches "dir_path":"<path>" in a JSON string.
*/
export function buildDirPathArgsPattern(dirPath: string): string {
return buildParamArgsPattern('dir_path', dirPath);
}
/**
@@ -112,7 +138,5 @@ export function buildFilePathArgsPattern(filePath: string): string {
* @returns A regex string that matches "pattern":"<pattern>" in a JSON string.
*/
export function buildPatternArgsPattern(pattern: string): string {
const encodedPattern = JSON.stringify(pattern);
// We use escapeRegex to ensure regex control characters are treated as literals.
return escapeRegex(`"pattern":${encodedPattern}`);
return buildParamArgsPattern('pattern', pattern);
}