Add CORS, rate limiting, security headers, and files endpoint protection

This commit is contained in:
Gan, Jimmy
2026-02-21 11:18:29 +08:00
parent 7b4c52f646
commit b1dee47126
5 changed files with 63 additions and 8 deletions
+6 -1
View File
@@ -3,11 +3,14 @@ import asyncio
import httpx
from fastapi import APIRouter, Depends, HTTPException, status, Request
from pydantic import BaseModel
from slowapi import Limiter
from slowapi.util import get_remote_address
import config
import auth
import pyotp
router = APIRouter()
limiter = Limiter(key_func=get_remote_address)
class LoginRequest(BaseModel):
username: str
@@ -50,6 +53,7 @@ async def send_failed_login_alert(ip_address: str, username: str, reason: str):
print(f"Failed to send Telegram alert: {e}", flush=True)
@router.post("/login", response_model=Token)
@limiter.limit("5/minute")
async def login(creds: LoginRequest, request: Request):
client_ip = request.client.host
# Verify username/password
@@ -121,7 +125,8 @@ class ChangePasswordRequest(BaseModel):
new_password: str
@router.post("/change-password")
async def change_password(req: ChangePasswordRequest, current_user: str = Depends(auth.get_current_user)):
@limiter.limit("5/minute")
async def change_password(req: ChangePasswordRequest, request: Request, current_user: str = Depends(auth.get_current_user)):
if not auth.verify_password(req.current_password, auth.load_password_hash()):
raise HTTPException(status_code=400, detail="Current password is incorrect")
if len(req.new_password) < 8:
+23 -5
View File
@@ -1,24 +1,33 @@
import os
import shutil
from pathlib import Path
from fastapi import APIRouter, UploadFile, File
from fastapi import APIRouter, UploadFile, File, HTTPException
from fastapi.responses import FileResponse
from config import VOLUME_ROOT
router = APIRouter()
BASE = Path(VOLUME_ROOT)
BLOCKED_DIRS = {"@appstore", "@docker", "@eaDir", "@S2S", "@sharesnap", "@tmp"}
def _safe_path(path: str) -> Path:
target = (BASE / path).resolve()
if not str(target).startswith(str(BASE.resolve())):
raise ValueError("forbidden")
# Block access to sensitive directories
rel = target.relative_to(BASE.resolve())
if rel.parts and rel.parts[0] in BLOCKED_DIRS:
raise ValueError("forbidden")
return target
@router.get("/browse")
def browse(path: str = ""):
target = _safe_path(path)
try:
target = _safe_path(path)
except ValueError:
raise HTTPException(status_code=403, detail="Access denied")
entries = []
for item in sorted(target.iterdir()):
try:
@@ -31,12 +40,18 @@ def browse(path: str = ""):
@router.get("/download")
def download(path: str):
return FileResponse(_safe_path(path))
try:
return FileResponse(_safe_path(path))
except ValueError:
raise HTTPException(status_code=403, detail="Access denied")
@router.post("/upload")
async def upload(path: str, file: UploadFile = File(...)):
target = _safe_path(path) / file.filename
try:
target = _safe_path(path) / file.filename
except ValueError:
raise HTTPException(status_code=403, detail="Access denied")
with open(target, "wb") as f:
shutil.copyfileobj(file.file, f)
return {"ok": True}
@@ -44,7 +59,10 @@ async def upload(path: str, file: UploadFile = File(...)):
@router.delete("/delete")
def delete(path: str):
target = _safe_path(path)
try:
target = _safe_path(path)
except ValueError:
raise HTTPException(status_code=403, detail="Access denied")
if target.is_dir():
shutil.rmtree(target)
else: