fix(validators): hard-block the token formats the run-length patterns missed
Review finding 91e21c1f (MAJOR emitted, catalogue tier BLOCKER). Live in every
session regardless of the STORM flag.
The hard-block tier is the one thing no operator flag unlocks - not `strict`,
not `--soft`, not VOYAGE_QUERY_PRIVACY_ALLOW=1 - so a format it misses is a
secret leaving the machine in an outbound query with no second gate behind it.
Two patterns keyed on a run of consecutive alphanumerics, which a `-` or `_`
inside the token body breaks:
- /\bsk-[A-Za-z0-9]{20,}\b/ was commented "OpenAI/Anthropic-style" but the run
ends after `api03` (3 chars) in an Anthropic Console key, so
sk-ant-api03-<~95> passed through. Measured by execution, not read.
- /\bghp_[A-Za-z0-9]{36,}\b/ covered only the classic prefix: github_pat_<...>
and gho_<36> passed through; ghp_<36> was blocked.
Widened with one pattern per real-world format rather than one loose pattern,
so each stays readable and its length floor stays honest:
- sk-ant-<scheme>-<20+ base64url> (covers api03 and oat01)
- gh[pousr]_<36+> (classic PAT, OAuth, user, server, refresh)
- github_pat_<20+ incl. underscore> (fine-grained, real format is <22>_<59>)
Patterns whose body class includes `-`/`_` carry no trailing \b - it would not
fire on a non-word final character. The existing sk-/AKIA/xox/PEM patterns are
unchanged; the AWS comment is accurate as written, so it was left alone.
Formats verified against GitHub's token-format documentation and Anthropic key
anatomy before the patterns were written, not from memory:
- github.blog/engineering/platform-security/behind-githubs-new-authentication-token-formats/
- gh[pousr]_ + 36 chars; github_pat_ + <22>_<59> = 93 total
- sk-ant-api03- + ~95 base64url chars (base64url includes _ and -)
6 new tests: three table rows for the missed formats, and three that pin each
one blocked with `strict: false` AND the opt-in env var set at once - the
property that makes this tier meaningful.
Suite 952 (950/0/2, baseline 937 + 15 across both Track A fixes).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zNqxP8qTWgJhn3wMYUFEh
This commit is contained in:
parent
fa2404b63c
commit
21a96b9e31
2 changed files with 42 additions and 2 deletions
|
|
@ -37,10 +37,19 @@ export const REPO_IDENTIFIER_PATTERNS = Object.freeze([
|
|||
]);
|
||||
|
||||
// HARD-BLOCK tier — secret-shaped strings. Never operator-overridable.
|
||||
//
|
||||
// Token bodies that contain `-` or `_` break a plain `[A-Za-z0-9]{n,}` run, so
|
||||
// each such format needs its own pattern rather than relying on run length:
|
||||
// an Anthropic Console key runs out after `api03` (3 alphanumerics), and a
|
||||
// fine-grained GitHub PAT after its 22-character segment. Patterns whose body
|
||||
// class includes `-`/`_` carry no trailing `\b`, which would not fire on a
|
||||
// non-word final character.
|
||||
export const SECRET_SHAPED_PATTERNS = Object.freeze([
|
||||
/\bsk-[A-Za-z0-9]{20,}\b/, // OpenAI/Anthropic-style API keys
|
||||
/\bsk-[A-Za-z0-9]{20,}\b/, // OpenAI-style API key (sk-<48>)
|
||||
/\bsk-ant-[a-z0-9]+-[A-Za-z0-9_-]{20,}/, // Anthropic Console key (sk-ant-api03-/-oat01- + ~95 base64url)
|
||||
/\bAKIA[0-9A-Z]{16}\b/, // AWS access key ID
|
||||
/\bghp_[A-Za-z0-9]{36,}\b/, // GitHub personal access token
|
||||
/\bgh[pousr]_[A-Za-z0-9]{36,}\b/, // GitHub classic PAT / OAuth / user / server / refresh token
|
||||
/\bgithub_pat_[A-Za-z0-9_]{20,}/, // GitHub fine-grained PAT (github_pat_<22>_<59>)
|
||||
/\bxox[baprs]-[A-Za-z0-9-]{10,}\b/, // Slack token
|
||||
/-----BEGIN [A-Z ]*PRIVATE KEY-----/, // PEM private key block
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -28,6 +28,16 @@ const FAKE_OPENAI_KEY = 'sk-' + 'a'.repeat(24);
|
|||
const FAKE_AWS_KEY = 'AKIA' + 'Q'.repeat(16);
|
||||
const FAKE_GITHUB_PAT = 'ghp_' + 'b'.repeat(36);
|
||||
|
||||
// Real-world formats whose token body contains hyphens/underscores. A run of
|
||||
// plain alphanumerics is broken by those separators, so a naive
|
||||
// `[A-Za-z0-9]{20,}` run-length pattern lets them through — the gap this file
|
||||
// pins. Anthropic Console keys are `sk-ant-api03-` + ~95 base64url chars;
|
||||
// GitHub fine-grained PATs are `github_pat_<22>_<59>`; `gho_` is the OAuth
|
||||
// sibling of the classic `ghp_` token.
|
||||
const FAKE_ANTHROPIC_KEY = 'sk-' + 'ant-' + 'api03-' + 'A1b2_-x9'.repeat(12);
|
||||
const FAKE_GITHUB_FINE_GRAINED = 'github' + '_pat_' + 'A'.repeat(22) + '_' + 'c'.repeat(59);
|
||||
const FAKE_GITHUB_OAUTH = 'gho' + '_' + 'd'.repeat(36);
|
||||
|
||||
function runShim(args) {
|
||||
try {
|
||||
const out = execFileSync(process.execPath, [SHIM, ...args], {
|
||||
|
|
@ -50,6 +60,9 @@ const TABLE = [
|
|||
{ label: 'secret-shaped (OpenAI/Anthropic-style key)', text: `auth failing with key ${FAKE_OPENAI_KEY}`, code: 'PRIVACY_SECRET_SHAPED' },
|
||||
{ label: 'secret-shaped (AWS access key)', text: `rotate ${FAKE_AWS_KEY} now`, code: 'PRIVACY_SECRET_SHAPED' },
|
||||
{ label: 'secret-shaped (GitHub PAT)', text: `token leaked: ${FAKE_GITHUB_PAT}`, code: 'PRIVACY_SECRET_SHAPED' },
|
||||
{ label: 'secret-shaped (Anthropic Console key)', text: `why does ${FAKE_ANTHROPIC_KEY} 401`, code: 'PRIVACY_SECRET_SHAPED' },
|
||||
{ label: 'secret-shaped (GitHub fine-grained PAT)', text: `pushed with ${FAKE_GITHUB_FINE_GRAINED}`, code: 'PRIVACY_SECRET_SHAPED' },
|
||||
{ label: 'secret-shaped (GitHub OAuth token)', text: `oauth flow returned ${FAKE_GITHUB_OAUTH}`, code: 'PRIVACY_SECRET_SHAPED' },
|
||||
];
|
||||
|
||||
for (const { label, text, code } of TABLE) {
|
||||
|
|
@ -104,6 +117,24 @@ test('validateOutboundQuery — VOYAGE_QUERY_PRIVACY_ALLOW=1 combined with a sec
|
|||
assert.equal(r.errors[0].code, 'PRIVACY_SECRET_SHAPED');
|
||||
});
|
||||
|
||||
// The hard-block tier is the one thing no operator flag unlocks, so a format
|
||||
// it misses is a secret leaving the machine with no second gate behind it.
|
||||
// Pin every real-world format against BOTH escape hatches at once.
|
||||
for (const [label, token] of [
|
||||
['Anthropic Console key', FAKE_ANTHROPIC_KEY],
|
||||
['GitHub fine-grained PAT', FAKE_GITHUB_FINE_GRAINED],
|
||||
['GitHub OAuth token', FAKE_GITHUB_OAUTH],
|
||||
]) {
|
||||
test(`validateOutboundQuery — ${label} stays blocked under --soft and the opt-in`, () => {
|
||||
const r = validateOutboundQuery(`leaked ${token}`, {
|
||||
strict: false,
|
||||
env: { VOYAGE_QUERY_PRIVACY_ALLOW: '1' },
|
||||
});
|
||||
assert.equal(r.valid, false, 'hard-block tier must never be overridable');
|
||||
assert.ok(r.errors.find(e => e.code === 'PRIVACY_SECRET_SHAPED'));
|
||||
});
|
||||
}
|
||||
|
||||
// ---- empty input --------------------------------------------------------------
|
||||
|
||||
test('validateOutboundQuery — empty string is invalid', () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue