fix: close library upload flow and harden image storage
Community checks / verify (push) Skipped
Verify Frame Flow / verify (push) Successful in 8m9s

This commit is contained in:
渡口浪人
2026-10-24 00:38:00 +08:00
parent 0859871363
commit 1494904b12
39 changed files with 1471 additions and 240 deletions
+45 -25
View File
@@ -4,6 +4,7 @@ import hashlib
import hmac
import json
import secrets
import threading
import time
from pathlib import Path
@@ -105,9 +106,12 @@ def session_user(request: Request):
def install_accounts(app, db):
# Serialize authentication work without holding the database lock during scrypt.
login_lock = threading.Lock()
@app.post("/api/v1/auth/login", response_model=AccountView, tags=["Accounts"])
def login(payload: Login, request: Request, response: Response):
with db.lock:
with login_lock:
# Reject login CSRF before checking credentials. Never trust forwarded hosts.
origin = request.headers.get("origin")
if request.headers.get("sec-fetch-site") == "cross-site" or (
@@ -154,31 +158,47 @@ def install_accounts(app, db):
(key, failures, time.time() + 300 if failures >= 5 else 0),
)
raise FlowError(E.UNAUTHORIZED, "Invalid username or password", 401)
for key in identities:
db.execute("DELETE FROM login_attempts WHERE identity=?", (key,))
if row["password_hash"].startswith("scrypt$"):
db.execute(
"UPDATE users SET password_hash=? WHERE username=?",
(password_hash(payload.password), row["username"]),
)
raw, csrf = secrets.token_urlsafe(32), secrets.token_urlsafe(32)
lifetime = 30 * 86400 if payload.remember else 12 * 3600
db.execute("DELETE FROM sessions WHERE expires_at<=?", (time.time(),))
old = request.cookies.get(COOKIE)
if old:
db.execute(
"DELETE FROM sessions WHERE token_hash=?",
(hashlib.sha256(old.encode()).hexdigest(),),
)
db.execute(
"INSERT INTO sessions VALUES(?,?,?,?)",
(
hashlib.sha256(raw.encode()).hexdigest(),
row["username"],
csrf,
time.time() + lifetime,
),
upgraded = (
password_hash(payload.password)
if row["password_hash"].startswith("scrypt$")
else None
)
with db.lock:
# A concurrent password change must not create a session from old credentials.
current_row = db.one(
"SELECT * FROM users WHERE username=? AND enabled=1",
(payload.username,),
)
if (
not current_row
or current_row["password_hash"] != row["password_hash"]
):
raise FlowError(E.UNAUTHORIZED, "Invalid username or password", 401)
for key in identities:
db.execute("DELETE FROM login_attempts WHERE identity=?", (key,))
if row["password_hash"].startswith("scrypt$"):
db.execute(
"UPDATE users SET password_hash=? WHERE username=?",
(upgraded, row["username"]),
)
raw, csrf = secrets.token_urlsafe(32), secrets.token_urlsafe(32)
lifetime = 30 * 86400 if payload.remember else 12 * 3600
db.execute("DELETE FROM sessions WHERE expires_at<=?", (time.time(),))
old = request.cookies.get(COOKIE)
if old:
db.execute(
"DELETE FROM sessions WHERE token_hash=?",
(hashlib.sha256(old.encode()).hexdigest(),),
)
db.execute(
"INSERT INTO sessions VALUES(?,?,?,?)",
(
hashlib.sha256(raw.encode()).hexdigest(),
row["username"],
csrf,
time.time() + lifetime,
),
)
response.set_cookie(
COOKIE,
raw,
+1
View File
@@ -14,6 +14,7 @@ bearer = HTTPBearer(auto_error=False)
ALL_SCOPES = [
"devices:read",
"images:read",
"images:write",
"images:delete",
"display:write",
"templates:read",