mirror of
https://github.com/Routstr/routstr-core.git
synced 2026-08-10 19:16:31 +00:00
harden(admin): cap scrypt work factor, keep generated password off disk
Address review hardening items on the secret-storage path: - vault.verify_password caps N/r/p at the parameters this module emits, so a tampered or corrupt stored hash can't force an unbounded scrypt work factor (memory grows with N*r) and turn a login into an OOM/DoS. - bootstrap prints the generated first-run admin password to stdout instead of the logger, so it reaches the operator once without being persisted into the on-disk log files. - admin_login reads the password hash while the DB session is open rather than off a detached ORM instance after the context exits. - drop a stray debug print of the request payload in upsert_provider_model. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
4872c318d5
commit
b8700dde40
@@ -343,11 +343,14 @@ async def admin_login(
|
||||
) -> dict[str, object]:
|
||||
async with create_session() as session:
|
||||
secret = await get_secret(session)
|
||||
# Read the hash while the session is open; the ORM object is detached
|
||||
# once the context exits and its attributes can no longer be loaded.
|
||||
password_hash = secret.admin_password_hash
|
||||
|
||||
if not secret.admin_password_hash:
|
||||
if not password_hash:
|
||||
raise HTTPException(status_code=500, detail="Admin password not configured")
|
||||
|
||||
if not vault.verify_password(payload.password, secret.admin_password_hash):
|
||||
if not vault.verify_password(payload.password, password_hash):
|
||||
raise HTTPException(status_code=401, detail="Invalid password")
|
||||
|
||||
token = secrets.token_urlsafe(32)
|
||||
@@ -526,7 +529,6 @@ class ModelCreate(BaseModel):
|
||||
async def upsert_provider_model(
|
||||
provider_id: str, payload: ModelCreate
|
||||
) -> dict[str, object]:
|
||||
print(payload)
|
||||
logger.info(
|
||||
f"UPSERT_PROVIDER_MODEL called: provider_id={provider_id}, model_id={payload.id}"
|
||||
)
|
||||
|
||||
@@ -467,9 +467,6 @@ async def bootstrap_secrets(db_session: AsyncSession) -> None:
|
||||
|
||||
from . import vault
|
||||
from .db import get_secret
|
||||
from .logging import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
raw_blob = await _read_raw_settings_blob(db_session)
|
||||
secret = await get_secret(db_session)
|
||||
@@ -486,12 +483,14 @@ async def bootstrap_secrets(db_session: AsyncSession) -> None:
|
||||
generated = secrets.token_urlsafe(24)
|
||||
secret.admin_password_hash = vault.hash_password(generated)
|
||||
admin_url = (settings.http_url or "http://localhost:8000").rstrip("/")
|
||||
logger.warning(
|
||||
# Print to stdout rather than the logger: the operator must see this
|
||||
# once (e.g. `docker compose logs`), but it must not be persisted
|
||||
# into the on-disk log files the logger also writes to.
|
||||
print(
|
||||
"No admin password set; generated a temporary one (shown only "
|
||||
"now): %s\nLog in at %s/admin and change it from the dashboard "
|
||||
"settings.",
|
||||
generated,
|
||||
admin_url,
|
||||
f"now): {generated}\nLog in at {admin_url}/admin and change it "
|
||||
"from the dashboard settings.",
|
||||
flush=True,
|
||||
)
|
||||
changed = True
|
||||
|
||||
|
||||
@@ -116,14 +116,20 @@ def verify_password(password: str, stored: str) -> bool:
|
||||
scheme, n, r, p, salt_b64, hash_b64 = stored.split(":")
|
||||
if scheme != "scrypt":
|
||||
return False
|
||||
n_int, r_int, p_int = int(n), int(r), int(p)
|
||||
# Cap the work factor at the parameters this module emits. scrypt's
|
||||
# memory cost grows with N*r, so an oversized N/r in a tampered or
|
||||
# corrupt stored hash could turn a single login into an OOM/DoS.
|
||||
if n_int > _SCRYPT_N or r_int > _SCRYPT_R or p_int > _SCRYPT_P:
|
||||
return False
|
||||
salt = base64.b64decode(salt_b64)
|
||||
expected = base64.b64decode(hash_b64)
|
||||
derived = hashlib.scrypt(
|
||||
password.encode(),
|
||||
salt=salt,
|
||||
n=int(n),
|
||||
r=int(r),
|
||||
p=int(p),
|
||||
n=n_int,
|
||||
r=r_int,
|
||||
p=p_int,
|
||||
dklen=len(expected),
|
||||
)
|
||||
except (ValueError, TypeError):
|
||||
|
||||
Reference in New Issue
Block a user