fix(#3267): harden collapsed tool-preview secret filter (Codex gate MUST-FIX)

Codex regression gate found the exact-name hidden-key set leaked secret-shaped
args (apiKey/access_token/clientSecret/Authorization/cookie/...) into the
always-visible collapsed tool-card header. Replace with a normalized
case-insensitive _toolArgPreviewKeyIsHidden() predicate matching secret-bearing
substrings + camelCase variants. Adds 22 parametrized regression tests pinning
the secret-key denial + a legit-key-still-shown guard. Co-authored-by preserved.
This commit is contained in:
nesquena-hermes
2026-05-31 19:21:15 +00:00
parent 639a88e937
commit 1aed605fb6
3 changed files with 57 additions and 3 deletions

View File

@@ -7030,17 +7030,30 @@ function _toolArgPreviewValue(value){
if(typeof value==='object') return 'object';
return String(value).replace(/\s+/g,' ').trim();
}
// Secret/sensitive-arg guard for collapsed tool-card previews. Exact-name hiding
// alone misses camelCase / variant spellings (apiKey, access_token, clientSecret,
// Authorization, …), so a normalized substring check runs first so secret-shaped
// argument names are never surfaced in the always-visible collapsed header (#3267).
function _toolArgPreviewKeyIsHidden(key){
const k=String(key||'').toLowerCase().replace(/[^a-z0-9]/g,'');
// verbose-but-not-secret bodies we keep out of the compact preview
const verbose=['content','filecontent','newstring','oldstring','patch','text','message','prompt','code','script','cookies','headers'];
if(verbose.includes(k)) return true;
// secret-shaped substrings (covers api_key/apiKey, access_token/auth_token/bearer,
// client_secret, password, credential, private_key, authorization, etc.)
return /(apikey|token|secret|password|passwd|credential|authorization|\bauth\b|auth$|^auth|bearer|privatekey|accesskey|sessionkey|signingkey|cookie)/.test(k)
|| k==='auth' || k==='key' || k==='pat';
}
function _formatToolArgPreview(args){
if(!args||typeof args!=='object') return '';
const preferred=['path','file_path','target','pattern','query','url','urls','name','ref','command','action','mode','schedule','workdir'];
const hidden=new Set(['content','file_content','new_string','old_string','patch','text','message','prompt','code','script','cookies','headers','auth','api_key','password','token','secret']);
const keys=[];
for(const key of preferred){
if(Object.prototype.hasOwnProperty.call(args,key)&&!hidden.has(key)) keys.push(key);
if(Object.prototype.hasOwnProperty.call(args,key)&&!_toolArgPreviewKeyIsHidden(key)) keys.push(key);
}
for(const key of Object.keys(args)){
if(keys.length>=3) break;
if(keys.includes(key)||hidden.has(key)) continue;
if(keys.includes(key)||_toolArgPreviewKeyIsHidden(key)) continue;
keys.push(key);
}
const parts=[];

View File

@@ -109,6 +109,8 @@ def test_rendered_apply_patch_tool_card_html_contains_diff_lines():
"_cliPatchSnippetFromArgs",
"_cliToolCardSnippet",
"_cliToolCardHasDiffSnippet",
"_toolArgPreviewValue",
"_toolArgPreviewKeyIsHidden",
"_formatToolArgPreview",
"_toolCardPreviewText",
"buildToolCard",

View File

@@ -36,6 +36,7 @@ function extractFunc(name) {
return src.slice(start, i);
}
eval(extractFunc('_toolArgPreviewValue'));
eval(extractFunc('_toolArgPreviewKeyIsHidden'));
eval(extractFunc('_formatToolArgPreview'));
eval(extractFunc('_toolCardPreviewText'));
let buf = '';
@@ -103,3 +104,41 @@ def test_explicit_progress_preview_still_wins(driver_path):
"stdout",
)
assert preview == "Running command"
@pytest.mark.parametrize(
"secret_key",
[
"api_key", "apiKey", "API_KEY", "x-api-key",
"token", "access_token", "refresh_token", "auth_token", "bearer_token",
"authorization", "Authorization",
"secret", "secret_key", "client_secret", "clientSecret",
"password", "passwd",
"private_key", "credential", "cookie", "cookies",
],
)
def test_collapsed_preview_never_exposes_secret_shaped_arg_keys(driver_path, secret_key):
"""Regression: the collapsed-preview arg summary must never surface a
secret-shaped argument key/value, including camelCase / variant spellings.
Codex regression gate found that exact-name hiding leaked apiKey /
access_token / clientSecret / Authorization etc. into the always-visible
collapsed header (v0.51.190). The normalized `_toolArgPreviewKeyIsHidden`
predicate closes this; this test pins it so it can't silently regress.
"""
preview = _preview(
driver_path,
{"name": "mcp_tool", "args": {secret_key: "SUPER-SECRET-VALUE-xyz", "path": "/visible/ok"}, "done": True},
)
assert "SUPER-SECRET-VALUE-xyz" not in preview, f"{secret_key} value leaked into preview: {preview!r}"
# the legit, non-secret key is still allowed to render
assert "/visible/ok" in preview
def test_collapsed_preview_still_shows_legit_keys(driver_path):
"""The secret guard must not over-block ordinary tool args."""
preview = _preview(
driver_path,
{"name": "search_files", "args": {"target": "content", "pattern": "foo", "workdir": "/repo"}, "done": True},
)
assert "target=" in preview and "pattern=" in preview