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
5 changes: 5 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,8 @@
**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.

## 2026-08-22 - [Sentinel: FastAPI `hmac.compare_digest` 비-ASCII DoS 취약점 수정]
**취약점:** 비-ASCII 문자가 포함된 `hmac.compare_digest` 문자열 비교로 인한 서비스 거부(DoS) 취약점 (CWE-400).
**학습:** `hmac.compare_digest()`는 비-ASCII 문자가 포함된 문자열을 비교할 때 `TypeError`를 발생시킵니다. 공격자가 HTTP 헤더(`x-api-key`) 등을 통해 악의적인 비-ASCII 문자열을 주입하면, 이를 처리하지 못하고 예외가 발생하여 서버 크래시(HTTP 500) 및 서비스 거부를 초래할 수 있습니다.
**예방:** 사용자 입력값과 설정된 키를 `hmac.compare_digest()`로 비교하기 전에 항상 `.encode('utf-8')`을 사용하여 바이트 객체로 변환해야 합니다. 성능을 위해 반복문 내부가 아닌 외부에서 미리 인코딩을 수행하는 것이 좋습니다.
3 changes: 2 additions & 1 deletion saas_web.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,9 @@ async def require_api_key(request: Request, call_next):
configured_keys = get_configured_api_keys()
if configured_keys and not (request.method == "GET" and request.url.path == "/"):
provided_key = request.headers.get("x-api-key", "")
provided_key_bytes = provided_key.encode("utf-8")
if not any(
hmac.compare_digest(provided_key, key) for key in configured_keys
hmac.compare_digest(provided_key_bytes, key.encode("utf-8")) for key in configured_keys

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Info: Configured keys re-encoded per request

key.encode("utf-8") at saas_web.py runs inside the generator, re-encoding every configured key on each request. The sentinel note added in this PR (sentinel.md) advises pre-encoding outside the loop. Impact is negligible, but the two disagree.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Info: API keys still read from os.environ

get_configured_api_keys reads keys via os.environ.get (saas_web.py), the runtime-secrets anti-pattern AGENTS.md marks for migration to the KV registry. Pre-existing and outside this PR's diff, so not reported as a bug, but this is the auth path being touched.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

):
return JSONResponse(
status_code=401,
Expand Down
26 changes: 26 additions & 0 deletions tests/test_saas_web.py
Original file line number Diff line number Diff line change
Expand Up @@ -1223,5 +1223,31 @@ def test_video_content_type_accepted_by_validator(self):
)


@unittest.skipUnless(
_HAS_FASTAPI, "fastapi not installed (optional integration dependency)"
)
class TestSaasWebAuth(unittest.IsolatedAsyncioTestCase):
async def test_require_api_key_non_ascii_dos_prevention(self):
from starlette.requests import Request
from starlette.responses import JSONResponse
import json

with patch.dict(os.environ, {"CODEC_CARVER_API_KEYS": "secret-key"}):
scope = {
"type": "http",
"method": "POST",
"path": "/api/upload",
"headers": [(b"x-api-key", "ö".encode("utf-8"))],
}
request = Request(scope)

async def mock_call_next(req):
return JSONResponse(status_code=200, content={"status": "ok"})

response = await saas_web.require_api_key(request, mock_call_next)
self.assertEqual(response.status_code, 401)
self.assertEqual(json.loads(response.body), {"error": "Invalid or missing API key"})


if __name__ == "__main__":
unittest.main()
Loading