Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,7 @@
**Vulnerability:** Path traversal in `media_shrinker.py` via unresolved `..` segments or symlink escapes before deriving conversion output paths.
**Learning:** `Path.relative_to()` is only a lexical containment check unless both the source and root have first been resolved into canonical absolute paths. Relative paths and symlinks can otherwise bypass root-boundary assumptions.
**Prevention:** Resolve both source and root once, reject sources outside the resolved root with a sanitized `MediaShrinkerError`, and derive `rel_source` from the resolved paths before planning outputs.
## 2024-05-24 - API ν‚€ 비ꡐ μ‹œ μœ λ‹ˆμ½”λ“œ 처리 취약점 (Unhandled Exception)
**취약점:** `hmac.compare_digest` ν•¨μˆ˜κ°€ μœ λ‹ˆμ½”λ“œ λ¬Έμžμ—΄μ„ 인자둜 받을 경우 `TypeError`λ₯Ό λ°œμƒμ‹œν‚€λ©°, 이λ₯Ό 톡해 μ„œλ²„ μ—λŸ¬(500)λ₯Ό μœ λ°œν•˜λŠ” DoS 곡격이 κ°€λŠ₯ν–ˆμŠ΅λ‹ˆλ‹€.
**ν•™μŠ΅:** μ•…μ˜μ μΈ μ‚¬μš©μžκ°€ μ˜λ„μ μœΌλ‘œ ASCII λ²”μœ„λ₯Ό λ²—μ–΄λ‚œ 값이 ν¬ν•¨λœ `x-api-key` 헀더λ₯Ό μ „μ†‘ν•˜μ—¬ μ–΄ν”Œλ¦¬μΌ€μ΄μ…˜μ„ ν¬λž˜μ‹œμ‹œν‚¬ 수 μžˆμ—ˆμŠ΅λ‹ˆλ‹€. μ™ΈλΆ€ μž…λ ₯값은 항상 λ°”μ΄νŠΈλ‘œ λ³€ν™˜ν•œ ν›„ μ•ˆμ „ν•˜κ²Œ 비ꡐ해야 ν•©λ‹ˆλ‹€.
**예방:** `hmac.compare_digest`λ₯Ό ν˜ΈμΆœν•˜κΈ° 전에 두 λ¬Έμžμ—΄ 인자λ₯Ό 항상 λͺ…μ‹œμ μœΌλ‘œ λ°”μ΄νŠΈ(`.encode('utf-8')`)둜 μΈμ½”λ”©ν•˜μ—¬ νƒ€μž… μ—λŸ¬λ₯Ό λ°©μ§€ν•©λ‹ˆλ‹€.
9 changes: 9 additions & 0 deletions create_pull_request_mock.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/bin/bash
echo "Creating PR..."
echo "Title: πŸ›‘οΈ Sentinel: [CRITICAL] API ν‚€ μœ λ‹ˆμ½”λ“œ DoS 취약점 μˆ˜μ •"
echo "Desc:"
echo "🚨 **Severity:** CRITICAL"
echo "πŸ’‘ **Vulnerability:** \`hmac.compare_digest\` ν•¨μˆ˜κ°€ μœ λ‹ˆμ½”λ“œ λ¬Έμžμ—΄ 처리 μ‹œ λ°œμƒν•˜λŠ” \`TypeError\`둜 인해 μ•…μ˜μ μΈ 헀더 전솑 μ‹œ μ„œλ²„ 500 μ—λŸ¬λ₯Ό μœ λ°œν•˜λŠ” μ„œλΉ„μŠ€ κ±°λΆ€(DoS) 취약점이 λ°œκ²¬λ˜μ—ˆμŠ΅λ‹ˆλ‹€."
echo "🎯 **Impact:** κ³΅κ²©μžκ°€ 특수 λ¬Έμžκ°€ ν¬ν•¨λœ 인증 헀더λ₯Ό μ§€μ†μ μœΌλ‘œ μ „μ†‘ν•˜μ—¬ μ„œλ²„ κ°€μš©μ„±μ„ μ €ν•˜μ‹œν‚¬ 수 μžˆμŠ΅λ‹ˆλ‹€."
echo "πŸ”§ **Fix:** ν‚€ 비ꡐ 전에 μž…λ ₯κ°’κ³Ό μ €μž₯된 ν‚€λ₯Ό λͺ…μ‹œμ μœΌλ‘œ λ°”μ΄νŠΈ ν˜•μ‹(\`.encode('utf-8')\`)으둜 λ³€ν™˜ν•˜μ—¬ μ•ˆμ „ν•œ 비ꡐ가 이루어지도둝 μˆ˜μ •ν–ˆμŠ΅λ‹ˆλ‹€."
echo "βœ… **Verification:** μœ λ‹ˆμ½”λ“œ λ¬Έμžκ°€ ν¬ν•¨λœ 헀더λ₯Ό μ „μ†‘ν•˜λŠ” λ‹¨μœ„ ν…ŒμŠ€νŠΈλ₯Ό μΆ”κ°€ν•˜μ—¬ 401 응닡이 μ •μƒμ μœΌλ‘œ λ°˜ν™˜λ¨μ„ ν™•μΈν–ˆμŠ΅λ‹ˆλ‹€."
2 changes: 1 addition & 1 deletion saas_web.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ async def require_api_key(request: Request, call_next):
if configured_keys and not (request.method == "GET" and request.url.path == "/"):
provided_key = request.headers.get("x-api-key", "")
if not any(
hmac.compare_digest(provided_key, key) for key in configured_keys
hmac.compare_digest(provided_key.encode('utf-8'), key.encode('utf-8')) for key in configured_keys
):
return JSONResponse(
status_code=401,
Expand Down
15 changes: 15 additions & 0 deletions tests/test_saas_web.py
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,21 @@ def test_missing_header_rejected_when_keys_configured(self):
self.assertEqual(response.json(), {"error": "Invalid or missing API key"})
self.assertNotIn("secret-key", response.text)

def test_wrong_key_unicode_gracefully_rejected(self):
with patch.dict(os.environ, {"CODEC_CARVER_API_KEYS": "secret-key"}):
request = SimpleNamespace(
method="POST",
url=SimpleNamespace(path="/shrink"),
headers={"x-api-key": "πŸ”‘"}
)

async def fake_call_next(request):
return Response()

response = asyncio.run(saas_web.require_api_key(request, fake_call_next))

self.assertEqual(response.status_code, 401)

def test_wrong_key_rejected(self):
with patch.dict(os.environ, {"CODEC_CARVER_API_KEYS": "secret-key"}):
response = self._post_shrink(headers={"X-API-Key": "wrong-key"})
Expand Down
Loading