fix: close library upload flow and harden image storage
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -14,6 +14,7 @@ bearer = HTTPBearer(auto_error=False)
|
||||
ALL_SCOPES = [
|
||||
"devices:read",
|
||||
"images:read",
|
||||
"images:write",
|
||||
"images:delete",
|
||||
"display:write",
|
||||
"templates:read",
|
||||
|
||||
Reference in New Issue
Block a user