fix(llm-security): close OpenAI legacy key recall gap in pre-edit-secrets hook
Bare/unquoted legacy OpenAI keys (no label assignment, no Bearer prefix) slipped past the pre-write secret-detection hook. Added a pattern anchored on the T3BlbkFJ base64 "OpenAI" watermark (vendor-documented shape), avoiding the collision-prone bare sk-+48alnum form. Failing tests first, full suite green (2184/0/6). The originally planned source for this fix — porting two entries from commons' secret-egress.json — turned out to be a false premise: that file is a byte-identical copy of this hook's own table, not a superset. The two missing names existed only as prose in commons' conformance/manifest.json, describing a different repo's (the guard's) unpublished Python table. gcp-service-account-json was measured NOT to be a gap (already covered by the existing PEM-block pattern); openai-api-key-legacy was the one real gap, closed here with a locally-authored pattern rather than an invented "port". Commons notified via coord-send that their secret-egress.json (count: 18) is now stale. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PYJX35KLH3rpS6pi7LHj8u
This commit is contained in:
parent
e1511f91aa
commit
088e45836c
2 changed files with 50 additions and 0 deletions
|
|
@ -41,6 +41,12 @@ const SECRET_PATTERNS = [
|
|||
{ name: 'Generic credential assignment', pattern: /(?:password|passwd|secret|token|api[_-]?key)\s*[=:]\s*['"][^'"]{8,}['"]/i },
|
||||
{ name: 'Authorization header with token', pattern: /[Bb]earer [A-Za-z0-9\-._~+/]{20,}/ },
|
||||
{ name: 'Database connection string', pattern: /(?:postgres|mysql|mongodb|redis):\/\/[^\s]+@[^\s]+/i },
|
||||
// OpenAI legacy API key (pre-2024 sk-<48 chars> shape). Anchored on the
|
||||
// T3BlbkFJ base64 "OpenAI" watermark embedded mid-token rather than a
|
||||
// bare sk-+48alnum shape, which would collide with sk-ant-/sk-proj- and
|
||||
// other unrelated sk-* tokens. Recall gap: bare/unquoted legacy keys
|
||||
// (no label assignment, no Bearer prefix) previously slipped through.
|
||||
{ name: 'OpenAI Legacy API Key', pattern: /\bsk-[A-Za-z0-9]{20}T3BlbkFJ[A-Za-z0-9]{20}\b/ },
|
||||
// v7.8.3 #13 — three-part JWT (header.payload.signature, base64url). The
|
||||
// 10-char part minimum keeps prose fragments (eyJabc.def.ghi) from tripping.
|
||||
// Kept last so a Bearer-header context reports as 'Authorization header'.
|
||||
|
|
|
|||
|
|
@ -59,6 +59,14 @@ const jwtToken = [
|
|||
'.', 'TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ',
|
||||
].join('');
|
||||
|
||||
// OpenAI legacy API key: sk- + 20 chars + the base64 "OpenAI" watermark
|
||||
// (T3BlbkFJ) + 20 more chars. The watermark anchor is what the shape check
|
||||
// keys on, per vendor-documented format (avoids the bare sk-+48alnum shape,
|
||||
// which collides with sk-ant-/sk-proj- and other unrelated sk-* tokens).
|
||||
const openaiLegacyKey = [
|
||||
'sk-', 'a'.repeat(20), 'T3BlbkFJ', 'b'.repeat(20),
|
||||
].join('');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -216,6 +224,42 @@ describe('pre-edit-secrets — bare provider keys (#13)', () => {
|
|||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// OpenAI legacy API key — recall gap. A bare/unquoted legacy key (no label
|
||||
// assignment, no Bearer prefix) previously slipped through: sk-ant-api03-
|
||||
// and sk-proj- are covered, but the pre-2024 sk-<48 chars> shape had no
|
||||
// dedicated pattern and only 'Generic credential assignment' or
|
||||
// 'Authorization header with token' happened to catch it in labeled/header
|
||||
// contexts — not when it appears bare, e.g. an unquoted env assignment.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('pre-edit-secrets — OpenAI legacy key recall gap', () => {
|
||||
it('blocks a bare legacy OpenAI key in an unquoted env assignment', async () => {
|
||||
const result = await runHook(SCRIPT, writePayload(
|
||||
'.env',
|
||||
`OPENAI_API_KEY=${openaiLegacyKey}`
|
||||
));
|
||||
assert.equal(result.code, 2);
|
||||
assert.match(result.stderr, /BLOCKED/);
|
||||
assert.match(result.stderr, /OpenAI/);
|
||||
});
|
||||
|
||||
it('blocks a bare legacy OpenAI key with no surrounding context at all', async () => {
|
||||
const result = await runHook(SCRIPT, writePayload('notes.txt', openaiLegacyKey));
|
||||
assert.equal(result.code, 2);
|
||||
assert.match(result.stderr, /BLOCKED/);
|
||||
assert.match(result.stderr, /OpenAI/);
|
||||
});
|
||||
|
||||
it('allows prose mentioning the sk- prefix without the T3BlbkFJ watermark', async () => {
|
||||
const result = await runHook(SCRIPT, writePayload(
|
||||
'docs/notes.md',
|
||||
`Legacy OpenAI keys start with sk- followed by 48 characters, e.g. sk-${'x'.repeat(48)}.`
|
||||
));
|
||||
assert.equal(result.code, 0);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ALLOW cases
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue