Skip to content

πŸ›‘οΈ Sentinel: [HIGH] JWT crit 헀더 검증 κΈ°λŠ₯ μΆ”κ°€ - #851

Open
seonghobae wants to merge 1 commit into
mainfrom
sentinel-jwt-crit-validation-17978932081618502390
Open

πŸ›‘οΈ Sentinel: [HIGH] JWT crit 헀더 검증 κΈ°λŠ₯ μΆ”κ°€#851
seonghobae wants to merge 1 commit into
mainfrom
sentinel-jwt-crit-validation-17978932081618502390

Conversation

@seonghobae

@seonghobae seonghobae commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

🚨 Severity: HIGH
πŸ’‘ Vulnerability: JWT crit (critical) 헀더가 κ²€μ¦λ˜μ§€ μ•Šμ•„ RFC 7515λ₯Ό μœ„λ°˜ν–ˆμŠ΅λ‹ˆλ‹€. κ³΅κ²©μžκ°€ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ΄ μ§€μ›ν•˜μ§€ μ•ŠλŠ” μ€‘μš”ν•œ ν™•μž₯을 ν¬ν•¨μ‹œμΌœ λ³΄μ•ˆ μ œμ–΄λ₯Ό μš°νšŒν•  수 μžˆμŠ΅λ‹ˆλ‹€.
🎯 Impact: λ¬΄μ‹œλœ critical νŒŒλΌλ―Έν„°λ‘œ μΈν•œ 높은 λ³΄μ•ˆ 우회 μœ„ν—˜.
πŸ”§ Fix: backend/app/auth.py의 _validate_jwt_header에 λͺ…μ‹œμ μΈ 검증을 μΆ”κ°€ν•˜μ—¬ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ΄ μ»€μŠ€ν…€ ν™•μž₯을 μ§€μ›ν•˜μ§€ μ•ŠμœΌλ―€λ‘œ crit 헀더가 ν¬ν•¨λœ λͺ¨λ“  JWTλ₯Ό κ±°λΆ€ν•˜λ„λ‘ λ³€κ²½ν–ˆμŠ΅λ‹ˆλ‹€. crit 길이와 μœ ν˜•μ— λŒ€ν•œ λ°©μ–΄ λ‘œμ§λ„ μΆ”κ°€ν–ˆμŠ΅λ‹ˆλ‹€.
βœ… Verification: 전체 pytest ν…ŒμŠ€νŠΈλ₯Ό μ‹€ν–‰ν•˜μ—¬ λͺ¨λ‘ ν†΅κ³Όν–ˆμœΌλ©°, backend/tests/test_auth_security.py에 μΆ”κ°€λœ μƒˆλ‘œμš΄ ν…ŒμŠ€νŠΈ μΌ€μ΄μŠ€λ₯Ό 톡해 crit 헀더가 ν¬ν•¨λœ 토큰이 401 응닡과 ν•¨κ»˜ κ±°λΆ€λ˜λŠ” 것을 ν™•μΈν–ˆμŠ΅λ‹ˆλ‹€.


PR created automatically by Jules for task 17978932081618502390 started by @seonghobae

Summary by CodeRabbit

  • 버그 μˆ˜μ •

    • JWT 인증 ν—€λ”μ˜ crit κ°’ ν˜•μ‹κ³Ό ν•­λͺ© 수λ₯Ό κ²€μ¦ν•©λ‹ˆλ‹€.
    • μ§€μ›λ˜μ§€ μ•ŠλŠ” critical extension이 ν¬ν•¨λœ 토큰을 κ±°λΆ€ν•©λ‹ˆλ‹€.
    • 잘λͺ»λœ JWT 헀더에 λŒ€ν•΄ HTTP 401 였λ₯˜λ₯Ό λ°˜ν™˜ν•΄ 인증 λ³΄μ•ˆμ„ κ°•ν™”ν–ˆμŠ΅λ‹ˆλ‹€.
  • ν…ŒμŠ€νŠΈ

    • 잘λͺ»λœ ν˜•μ‹, λΉ„λ¬Έμžμ—΄ ν•­λͺ©, μ§€μ›λ˜μ§€ μ•ŠλŠ” extension에 λŒ€ν•œ λ³΄μ•ˆ 검증을 μΆ”κ°€ν–ˆμŠ΅λ‹ˆλ‹€.

JWT의 `crit` (critical) 헀더가 κ²€μ¦λ˜μ§€ μ•Šμ•„ RFC 7515λ₯Ό μœ„λ°˜ν•˜λŠ” λ³΄μ•ˆ 취약점을 μˆ˜μ •ν–ˆμŠ΅λ‹ˆλ‹€.
μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ΄ 인식할 수 μ—†λŠ” critical ν™•μž₯ κΈ°λŠ₯을 ν¬ν•¨ν•œ 토큰이 쑰용히 ν†΅κ³Όλ˜λŠ” 문제λ₯Ό λ°©μ§€ν•˜κΈ° μœ„ν•΄ `_validate_jwt_header`에 검증 λ‘œμ§μ„ μΆ”κ°€ν–ˆμŠ΅λ‹ˆλ‹€. ν˜„μž¬ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ€ ν™•μž₯ κΈ°λŠ₯을 μ§€μ›ν•˜μ§€ μ•ŠμœΌλ―€λ‘œ `crit` 헀더가 μ‘΄μž¬ν•˜λ©΄ 401 였λ₯˜λ₯Ό λ°˜ν™˜ν•©λ‹ˆλ‹€.
@google-labs-jules

Copy link
Copy Markdown

πŸ‘‹ Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a πŸ‘€ emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

πŸ“ Walkthrough

Walkthrough

JWT 인증 κ²½λ‘œμ— crit 헀더 검증이 μΆ”κ°€λ˜μ—ˆμŠ΅λ‹ˆλ‹€. 잘λͺ»λœ ν˜•μ‹, ν•­λͺ© 수, ν•­λͺ© νƒ€μž…, μ§€μ›λ˜μ§€ μ•ŠλŠ” ν™•μž₯을 ν¬ν•¨ν•œ 토큰은 HTTP 401둜 κ±°λΆ€λ©λ‹ˆλ‹€. κ΄€λ ¨ λ³΄μ•ˆ ν…ŒμŠ€νŠΈμ™€ PR μ„€λͺ…이 κ°±μ‹ λ˜μ—ˆμŠ΅λ‹ˆλ‹€.

Changes

JWT crit 헀더 검증

Layer / File(s) Summary
crit 헀더 검증 κ΅¬ν˜„
.jules/sentinel.md, backend/app/auth.py, pr_body.txt
crit ν—€λ”μ˜ λ°°μ—΄ ν˜•μ‹κ³Ό μ΅œλŒ€ 10개 ν•­λͺ©μ„ κ²€μ¦ν•©λ‹ˆλ‹€. 각 ν•­λͺ©μ€ λ¬Έμžμ—΄μ΄μ–΄μ•Ό ν•©λ‹ˆλ‹€. μ§€μ›λ˜μ§€ μ•ŠλŠ” critical extension이 있으면 인증 였λ₯˜λ₯Ό λ°˜ν™˜ν•©λ‹ˆλ‹€. λ³΄μ•ˆ 기둝과 PR μ„€λͺ…에 λ³€κ²½ λ‚΄μš©μ΄ λ°˜μ˜λ˜μ—ˆμŠ΅λ‹ˆλ‹€.
검증 μ‹€νŒ¨ 경둜 ν…ŒμŠ€νŠΈ
backend/tests/test_auth_security.py
잘λͺ»λœ crit νƒ€μž…, λΉ„λ¬Έμžμ—΄ ν•­λͺ©, μ§€μ›λ˜μ§€ μ•ŠλŠ” b32 ν™•μž₯에 λŒ€ν•΄ HTTP 401κ³Ό 였λ₯˜ μ„ΈλΆ€ λ©”μ‹œμ§€λ₯Ό κ²€μ¦ν•©λ‹ˆλ‹€.

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

Possibly related PRs

  • ContextualWisdomLab/pg-erd-cloud#743: λ™μΌν•œ JWT 처리 ν•¨μˆ˜μ™€ κ΄€λ ¨ λ³΄μ•ˆ ν…ŒμŠ€νŠΈλ₯Ό μˆ˜μ •ν•©λ‹ˆλ‹€.
  • ContextualWisdomLab/pg-erd-cloud#793: _validate_jwt_headerμ—μ„œ μ§€μ›λ˜μ§€ μ•ŠλŠ” JWT crit 헀더λ₯Ό κ±°λΆ€ν•˜κ³  κ΄€λ ¨ ν…ŒμŠ€νŠΈλ₯Ό μΆ”κ°€ν•©λ‹ˆλ‹€.
πŸš₯ 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 제λͺ©μ€ JWT crit 헀더 검증 μΆ”κ°€λΌλŠ” λ³€κ²½μ˜ μ£Όμš” λ‚΄μš©μ„ λͺ…ν™•ν•˜κ²Œ μš”μ•½ν•©λ‹ˆλ‹€.
Docstring Coverage βœ… Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 sentinel-jwt-crit-validation-17978932081618502390

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
backend/tests/test_auth_security.py (1)

570-571: πŸ“ Maintainability & Code Quality | πŸ”΅ Trivial | ⚑ Quick win

μƒˆ 쀑첩 mock ν•¨μˆ˜μ— λͺ…μ‹œμ  νƒ€μž…μ„ μΆ”κ°€ν•˜μ„Έμš”.

backend/**/*.pyλŠ” μ—„κ²©ν•œ νƒ€μž…μ„ μš”κ΅¬ν•©λ‹ˆλ‹€. μ„Έ mock ν•¨μˆ˜μ˜ _token λ§€κ°œλ³€μˆ˜μ™€ λ°˜ν™˜ νƒ€μž…μ΄ μ•”μ‹œμ μž…λ‹ˆλ‹€. μ €μž₯μ†Œμ˜ header νƒ€μž… λ˜λŠ” dict[str, Any]λ₯Ό μ‚¬μš©ν•˜μ—¬ 각 μ‹œκ·Έλ‹ˆμ²˜λ₯Ό λͺ…μ‹œν•˜μ„Έμš”.

Also applies to: 579-580, 588-589

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/tests/test_auth_security.py` around lines 570 - 571, Update all three
nested mock functions around mock_get_unverified_header_bad_crit to add explicit
types for the _token parameter and return value, using the repository’s header
type or dict[str, Any].

Source: Coding guidelines

πŸ€– Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/app/auth.py`:
- Around line 188-196: Update _validate_jwt_header’s crit validation to
distinguish an absent key from crit: null, require crit to be a non-empty list
of at most 10 items, and reject invalid values with HTTP 401 before iterating.
Add regression coverage for null, an empty array, and 11-item crit arrays.

In `@backend/tests/test_auth_security.py`:
- Around line 566-595: Extend test_oidc_decode_rejects_jwt_crit_header to cover
crit=[] , crit=None, and a list containing 11 entries. For each mocked header,
call auth._decode_verified_oidc_token and assert HTTPException status_code is
401 with detail "invalid crit header", preserving the existing
unsupported-extension assertion.

---

Nitpick comments:
In `@backend/tests/test_auth_security.py`:
- Around line 570-571: Update all three nested mock functions around
mock_get_unverified_header_bad_crit to add explicit types for the _token
parameter and return value, using the repository’s header type or dict[str,
Any].
πŸͺ„ Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
βš™οΈ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6837b815-06af-42fe-b778-269c97d13207

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 72afe6d and 97d6811.

πŸ“’ Files selected for processing (4)
  • .jules/sentinel.md
  • backend/app/auth.py
  • backend/tests/test_auth_security.py
  • pr_body.txt

Comment thread backend/app/auth.py
Comment on lines +188 to +196
crit = header.get("crit")
if crit is not None:
if not isinstance(crit, list) or len(crit) > 10:
raise HTTPException(status_code=401, detail="invalid crit header")
for item in crit:
if not isinstance(item, str):
raise HTTPException(status_code=401, detail="invalid crit header")
# We do not support any critical extensions.
raise HTTPException(status_code=401, detail="unsupported critical extension")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ”’ Security & Privacy | 🟠 Major | ⚑ Quick win

crit의 쑴재 여뢀와 λΉ„μ–΄ μžˆμ§€ μ•Šμ€ λ°°μ—΄ 쑰건을 κ²€μ¦ν•˜μ„Έμš”.

ν˜„μž¬ header.get("crit")λŠ” ν‚€κ°€ μ—†λŠ” κ²½μš°μ™€ crit: null을 λ™μΌν•˜κ²Œ μ²˜λ¦¬ν•©λ‹ˆλ‹€. crit: []도 길이 검사와 λ°˜λ³΅λ¬Έμ„ ν†΅κ³Όν•©λ‹ˆλ‹€. λ”°λΌμ„œ 잘λͺ»λœ crit 헀더가 _validate_jwt_headerλ₯Ό ν†΅κ³Όν•©λ‹ˆλ‹€. if "crit" in header둜 쑴재 μ—¬λΆ€λ₯Ό ν™•μΈν•˜κ³ , λ¦¬μŠ€νŠΈκ°€ μ•„λ‹ˆκ±°λ‚˜ λΉ„μ–΄ μžˆκ±°λ‚˜ 10개λ₯Ό μ΄ˆκ³Όν•˜λ©΄ 401둜 κ±°λΆ€ν•˜μ„Έμš”. null, 빈 λ°°μ—΄, 11개 ν•­λͺ©μ— λŒ€ν•œ νšŒκ·€ ν…ŒμŠ€νŠΈλ„ μΆ”κ°€ν•˜μ„Έμš”.

μˆ˜μ • μ˜ˆμ‹œ
-    crit = header.get("crit")
-    if crit is not None:
-        if not isinstance(crit, list) or len(crit) > 10:
+    if "crit" in header:
+        crit = header["crit"]
+        if not isinstance(crit, list) or not crit or len(crit) > 10:
             raise HTTPException(status_code=401, detail="invalid crit header")
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
crit = header.get("crit")
if crit is not None:
if not isinstance(crit, list) or len(crit) > 10:
raise HTTPException(status_code=401, detail="invalid crit header")
for item in crit:
if not isinstance(item, str):
raise HTTPException(status_code=401, detail="invalid crit header")
# We do not support any critical extensions.
raise HTTPException(status_code=401, detail="unsupported critical extension")
if "crit" in header:
crit = header["crit"]
if not isinstance(crit, list) or not crit or len(crit) > 10:
raise HTTPException(status_code=401, detail="invalid crit header")
for item in crit:
if not isinstance(item, str):
raise HTTPException(status_code=401, detail="invalid crit header")
# We do not support any critical extensions.
raise HTTPException(status_code=401, detail="unsupported critical extension")
πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/app/auth.py` around lines 188 - 196, Update _validate_jwt_header’s
crit validation to distinguish an absent key from crit: null, require crit to be
a non-empty list of at most 10 items, and reject invalid values with HTTP 401
before iterating. Add regression coverage for null, an empty array, and 11-item
crit arrays.

Source: Coding guidelines

Comment on lines +566 to +595
@pytest.mark.asyncio
async def test_oidc_decode_rejects_jwt_crit_header(
monkeypatch: pytest.MonkeyPatch,
) -> None:
def mock_get_unverified_header_bad_crit(_token):
return {"kid": "key-1", "alg": "RS256", "crit": "not-a-list"}

monkeypatch.setattr(auth.jwt, "get_unverified_header", mock_get_unverified_header_bad_crit)
with pytest.raises(HTTPException) as excinfo:
await auth._decode_verified_oidc_token("invalid_token")
assert excinfo.value.status_code == 401
assert excinfo.value.detail == "invalid crit header"

def mock_get_unverified_header_bad_crit_item(_token):
return {"kid": "key-1", "alg": "RS256", "crit": [123]}

monkeypatch.setattr(auth.jwt, "get_unverified_header", mock_get_unverified_header_bad_crit_item)
with pytest.raises(HTTPException) as excinfo:
await auth._decode_verified_oidc_token("invalid_token")
assert excinfo.value.status_code == 401
assert excinfo.value.detail == "invalid crit header"

def mock_get_unverified_header_unsupported_crit(_token):
return {"kid": "key-1", "alg": "RS256", "crit": ["b32"]}

monkeypatch.setattr(auth.jwt, "get_unverified_header", mock_get_unverified_header_unsupported_crit)
with pytest.raises(HTTPException) as excinfo:
await auth._decode_verified_oidc_token("invalid_token")
assert excinfo.value.status_code == 401
assert excinfo.value.detail == "unsupported critical extension"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ”’ Security & Privacy | 🟑 Minor | ⚑ Quick win

crit 경계값 νšŒκ·€ ν…ŒμŠ€νŠΈλ₯Ό μΆ”κ°€ν•˜μ„Έμš”.

ν˜„μž¬ ν…ŒμŠ€νŠΈλŠ” λΉ„λ¦¬μŠ€νŠΈ κ°’, λΉ„λ¬Έμžμ—΄ ν•­λͺ©, μ§€μ›λ˜μ§€ μ•ŠλŠ” ν™•μž₯만 ν™•μΈν•©λ‹ˆλ‹€. crit=[], crit=None, 11개 ν•­λͺ©μ€ ν™•μΈν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. 이 λˆ„λ½μœΌλ‘œ Line 188의 ν•˜ν•œ 쑰건과 λͺ…μ‹œμ  null 처리λ₯Ό λ³΄ν˜Έν•˜μ§€ λͺ»ν•©λ‹ˆλ‹€. 각 μž…λ ₯μ—μ„œ 401κ³Ό "invalid crit header"λ₯Ό ν™•μΈν•˜μ„Έμš”.

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/tests/test_auth_security.py` around lines 566 - 595, Extend
test_oidc_decode_rejects_jwt_crit_header to cover crit=[] , crit=None, and a
list containing 11 entries. For each mocked header, call
auth._decode_verified_oidc_token and assert HTTPException status_code is 401
with detail "invalid crit header", preserving the existing unsupported-extension
assertion.

Source: Coding guidelines

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant