Skip to content

fix(logs): record the configured shadow prefix, not the caller's model string - #2170

Merged
lidge-jun merged 1 commit into
devfrom
codex/audit-shadow-marker-leak
Aug 20, 2026
Merged

fix(logs): record the configured shadow prefix, not the caller's model string#2170
lidge-jun merged 1 commit into
devfrom
codex/audit-shadow-marker-leak

Conversation

@lidge-jun

@lidge-jun lidge-jun commented Aug 20, 2026

Copy link
Copy Markdown
Owner

Summary

Release audit of origin/main..origin/dev found a credential-leak path in the shadow-call logging that landed in this range. This is the fix.

The defect. The shadow-call intercept matches by prefix, so a caller can send gpt-5.6-luna followed by arbitrary text and still be intercepted. The whole raw string was then recorded as shadowCallRewrittenFrom, which is persisted to usage.jsonl and served from /api/logs.

The sanitizer on that path is not sufficient protection, for two independent reasons — both verified against the shipped code:

"gpt-5.6-luna\nBearer sk-abc123def456ghi789jkl"  ->  "gpt-5.6-lunaBearer [REDACTED]"
"gpt-5.6-luna\nAIzaSyA1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6"  ->  unchanged, key intact
  1. Control characters are stripped before redaction runs, so the newline that separated the marker from the credential is gone by the time the Bearer rule looks for a word boundary.
  2. The runtime redactor is a deny-list. An AIza-shaped Google key has no rule at all and survives verbatim into the log file.

The fix. Record the operator-configured prefix that matched, not the caller's string. shadowSourceModelPrefix() returns the entry from sourceModels that produced the match, so the field can only ever hold a value the operator configured. That removes the class rather than adding one more pattern to a deny-list — which is the right shape here, because the next unrecognized credential family would reopen it.

Verification

  • RED-first. Reverting only src/lib/shadow-call.ts and src/server/responses/core.ts fails exactly the two new tests (15 pass / 2 fail). The first pins the AIza smuggling case; the second pins that a non-default configured prefix is recorded as itself, so the fix cannot degrade into a hardcoded string.
  • bun x tsc --noEmit — exit 0 (on ssh lidge).
  • bun test --isolate tests/responses-shadow-intercept.test.ts — 17 pass / 0 fail.
  • bun run test on ssh lidge13713 pass / 15 skip / 0 fail across 866 files.
  • bun run privacy:scan — passed.

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs or release notes were updated when needed.
  • Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults.

This is the security-sensitive change: it narrows what can reach a persisted log field. No credential is logged by the new code, and the field's value set is now bounded by operator configuration.

Summary by CodeRabbit

  • Security & Privacy

    • Shadow-call logs now record only the configured source-model prefix instead of the full caller-provided model identifier.
    • Prevents credential-like or otherwise sensitive content appended to model IDs from being persisted.
  • Bug Fixes

    • Improved handling of custom source-model prefixes and routed model identifiers.
  • Tests

    • Added regression coverage for sanitized logging and custom prefix configurations.

…l string

The release audit of origin/main..origin/dev found this: the shadow-call
intercept matches by prefix, so a caller can send "gpt-5.6-luna" followed
by arbitrary text and still be intercepted -- and the whole raw string was
recorded as shadowCallRewrittenFrom, which is persisted to usage.jsonl and
served from /api/logs.

The sanitizer on that path strips control characters BEFORE redacting, so
"gpt-5.6-luna\nBearer sk-..." became "gpt-5.6-lunaBearer sk-...", losing the
word boundary the Bearer rule needs. Worse, the runtime redactor is a
deny-list: an AIza-shaped Google key has no rule at all and survived
verbatim into the log file. Verified against the shipped sanitizer.

Recording the operator-configured prefix that matched removes the class
instead of adding another pattern to the deny-list. No caller-controlled
string reaches the field, so the value is always one the operator chose.
@lidge-jun
lidge-jun requested a review from Ingwannu as a code owner August 20, 2026 04:00
@github-actions

Copy link
Copy Markdown
Contributor

Deterministic PR hygiene checks passed.

@github-actions github-actions Bot added the bug Something isn't working label Aug 20, 2026
@coderabbitai

coderabbitai Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f770cda6-84f6-4ff2-8b59-55ca65c97e7e

📥 Commits

Reviewing files that changed from the base of the PR and between c5fc969 and e928ed7.

📒 Files selected for processing (3)
  • src/lib/shadow-call.ts
  • src/server/responses/core.ts
  • tests/responses-shadow-intercept.test.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 7 remain after this review.


📝 Walkthrough

Walkthrough

The change adds configured source-model prefix matching and uses it for shadow-call rewrite logging. Request-path tests verify that raw model suffixes, including newline-appended credential-like content, are not recorded.

Changes

Shadow-call attribution

Layer / File(s) Summary
Configured prefix matching
src/lib/shadow-call.ts
Adds shadowSourceModelPrefix, which returns a matching configured prefix for bare model IDs and returns undefined for routed or unmatched IDs.
Sanitized rewrite logging and validation
src/server/responses/core.ts, tests/responses-shadow-intercept.test.ts
Uses the matched prefix for shadowCallRewrittenFrom. Tests cover default prefixes, custom prefixes, and newline-appended credential-like text.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: ⚪ Minimal · up to e928e

The change limits persisted shadow-call log values to configured prefixes and has passing targeted, type-check, full-suite, and privacy-scan verification; no actionable merge-blocking risk remains beyond normal checks.

Possibly related PRs

  • lidge-jun/opencodex#2163: Both changes modify shadow-call attribution in src/server/responses/core.ts; this PR changes the recorded value, while that PR adds attribution persistence and hydration.
  • lidge-jun/opencodex#2166: Both changes modify shadow-call rewrite logging; this PR records the configured source-model prefix, while that PR sanitizes persistence and log handling.

Suggested reviewers: ingwannu

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: logging the configured shadow prefix instead of the caller-provided model string.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/audit-shadow-marker-leak

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@lidge-jun

Copy link
Copy Markdown
Owner Author

리뷰 · 우선순위 74 / 80

실화임. 섀도 인터셉트가 prefix 매칭이라 호출자가 gpt-5.6-luna 뒤에 아무 텍스트를 붙여도 잡힘. 그 원문 전체가 shadowCallRewrittenFrom으로 usage.jsonl이랑 /api/logs에 남음. 새니타이저가 보호가 아님. 제어문자를 레드액션보다 먼저 지워서 gpt-5.6-luna\nBearer sk-...gpt-5.6-lunaBearer [REDACTED]가 됨. AIza 구글 키는 deny-list에 없어서 그대로 파일에 남음. privacy:scan은 레포만 봐서 런타임 로그 구멍을 못 봄.

픽이 맞음. 호출자 문자열이 아니라 매칭된 sourceModels prefix만 기록. 필드가 오퍼레이터가 설정한 값만 담음. 패턴 하나 더 넣는 게 아님. 다음 키 패밀리가 다시 안 열림. types.ts/config.ts 안 건드림. MERGEABLE.

점수는 74임. 2.28 블로커. #2166 sanitizer 위에 있는 구멍이라 그거 넣었다고 안 막힘.

해결방안: 머지. AIza 밀수 테스트랑 non-default prefix 테스트 유지. deny-list 확장으로 대체하지 말 것.

이 댓글은 grok-bot이 작성했습니다

@lidge-jun
lidge-jun merged commit 9eb6647 into dev Aug 20, 2026
43 of 45 checks passed
@lidge-jun
lidge-jun deleted the codex/audit-shadow-marker-leak branch August 20, 2026 13:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant