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
27 changes: 24 additions & 3 deletions api/auth.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import hmac
from typing import Optional

from fastapi import Depends, HTTPException, Request
Expand All @@ -10,10 +11,26 @@
from open_notebook.utils.encryption import get_secret_from_env


def _credential_bytes(credential: str) -> bytes:
"""
Recover the raw bytes a client sent for an Authorization credential.

ASGI servers decode header values as latin-1, so re-encoding with latin-1
restores the exact bytes on the wire and lets a UTF-8 password (including
characters outside latin-1) match. A credential that is not latin-1
encodable did not arrive over HTTP -- it came from a direct call -- so
fall back to UTF-8 for those callers.
"""
try:
return credential.encode("latin-1")
except UnicodeEncodeError:
return credential.encode("utf-8")


class PasswordAuthMiddleware(BaseHTTPMiddleware):
"""
Middleware to check password authentication for all API requests.
Always active with default password if OPEN_NOTEBOOK_PASSWORD is not set.
Disabled entirely if OPEN_NOTEBOOK_PASSWORD is not set.
Supports Docker secrets via OPEN_NOTEBOOK_PASSWORD_FILE.
"""

Expand Down Expand Up @@ -68,7 +85,9 @@ async def dispatch(
)

# Check password
if credentials != self.password:
if not hmac.compare_digest(
_credential_bytes(credentials), self.password.encode("utf-8")
):
return JSONResponse(
status_code=401,
content={"detail": "Invalid password"},
Expand Down Expand Up @@ -109,7 +128,9 @@ def check_api_password(
)

# Check password
if credentials.credentials != password:
if not hmac.compare_digest(
_credential_bytes(credentials.credentials), password.encode("utf-8")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: A direct call to check_api_password with a Latin-1-representable non-ASCII password (e.g. pässwörd) now always returns 401. _credential_bytes Latin-1-encodes the string before comparing to the configured password's UTF-8 bytes, and those encodings differ, so the credential can never match. The UTF-8 fallback only fires for strings outside Latin-1, so a direct caller with a Latin-1-encodable password is broken while one with, say, CJK characters works — inconsistent. Before this change the function compared strings directly and the correct password matched. Since check_api_password has no way to distinguish a direct caller from an over-HTTP credential when the string is Latin-1 encodable, apply the UTF-8 re-encoding for the direct-call path (or gate the Latin-1 wire-byte recovery to the middleware, which always sees HTTP-decoded headers).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At api/auth.py, line 132:

<comment>A direct call to `check_api_password` with a Latin-1-representable non-ASCII password (e.g. `pässwörd`) now always returns 401. `_credential_bytes` Latin-1-encodes the string before comparing to the configured password's UTF-8 bytes, and those encodings differ, so the credential can never match. The UTF-8 fallback only fires for strings outside Latin-1, so a direct caller with a Latin-1-encodable password is broken while one with, say, CJK characters works — inconsistent. Before this change the function compared strings directly and the correct password matched. Since `check_api_password` has no way to distinguish a direct caller from an over-HTTP credential when the string is Latin-1 encodable, apply the UTF-8 re-encoding for the direct-call path (or gate the Latin-1 wire-byte recovery to the middleware, which always sees HTTP-decoded headers).</comment>

<file context>
@@ -109,7 +128,9 @@ def check_api_password(
     # Check password
-    if credentials.credentials != password:
+    if not hmac.compare_digest(
+        _credential_bytes(credentials.credentials), password.encode("utf-8")
+    ):
         raise HTTPException(
</file context>

):
raise HTTPException(
status_code=401,
detail="Invalid password",
Expand Down
Loading