Add CORS, rate limiting, security headers, and files endpoint protection
This commit is contained in:
@@ -27,3 +27,6 @@ VPS_SSH_KEY_PATH = os.environ.get("VPS_SSH_KEY_PATH", "/app/ssh/vps_id_ed25519")
|
||||
TELEGRAM_BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN", "")
|
||||
TELEGRAM_CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID", "")
|
||||
TELEGRAM_PROXY = os.environ.get("TELEGRAM_PROXY", "")
|
||||
|
||||
# CORS
|
||||
CORS_ORIGINS = os.environ.get("CORS_ORIGINS", "https://nas.jimmygan.com").split(",")
|
||||
|
||||
@@ -1,13 +1,41 @@
|
||||
from fastapi import FastAPI, Depends
|
||||
from fastapi import FastAPI, Depends, Request
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
from slowapi import Limiter
|
||||
from slowapi.util import get_remote_address
|
||||
from slowapi.errors import RateLimitExceeded
|
||||
from routers import docker_router, gitea, files, terminal, system, auth
|
||||
import auth as auth_module
|
||||
|
||||
import asyncio
|
||||
import httpx
|
||||
from config import TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID
|
||||
from config import TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID, CORS_ORIGINS
|
||||
|
||||
limiter = Limiter(key_func=get_remote_address)
|
||||
app = FastAPI(title="NAS Dashboard")
|
||||
app.state.limiter = limiter
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=CORS_ORIGINS,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
@app.exception_handler(RateLimitExceeded)
|
||||
async def rate_limit_handler(request: Request, exc: RateLimitExceeded):
|
||||
return JSONResponse(status_code=429, content={"detail": "Too many requests, try again later"})
|
||||
|
||||
@app.middleware("http")
|
||||
async def security_headers(request: Request, call_next):
|
||||
response = await call_next(request)
|
||||
response.headers["X-Content-Type-Options"] = "nosniff"
|
||||
response.headers["X-Frame-Options"] = "DENY"
|
||||
response.headers["X-XSS-Protection"] = "1; mode=block"
|
||||
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
|
||||
return response
|
||||
|
||||
async def monitor_containers():
|
||||
import docker
|
||||
|
||||
@@ -8,3 +8,4 @@ pyjwt==2.8.0
|
||||
passlib==1.7.4
|
||||
pyotp==2.9.0
|
||||
asyncssh==2.17.0
|
||||
slowapi==0.1.9
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 = ""):
|
||||
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):
|
||||
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(...)):
|
||||
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):
|
||||
try:
|
||||
target = _safe_path(path)
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
if target.is_dir():
|
||||
shutil.rmtree(target)
|
||||
else:
|
||||
|
||||
Reference in New Issue
Block a user