Skip to content

Commit 1c117ef

Browse files
sfi issue fixes
1 parent 6d322c6 commit 1c117ef

File tree

1 file changed

+71
-7
lines changed

1 file changed

+71
-7
lines changed

src/frontend/frontend_server.py

Lines changed: 71 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
import uvicorn
44
from dotenv import load_dotenv
5-
from fastapi import FastAPI
5+
from pathlib import Path
6+
from fastapi import FastAPI, HTTPException, Request
67
from fastapi.middleware.cors import CORSMiddleware
78
from fastapi.responses import FileResponse, HTMLResponse
89
from fastapi.staticfiles import StaticFiles
@@ -23,12 +24,30 @@
2324
BUILD_DIR = os.path.join(os.path.dirname(__file__), "dist")
2425
INDEX_HTML = os.path.join(BUILD_DIR, "index.html")
2526

27+
# Resolved build directory path (used to prevent path traversal)
28+
BUILD_DIR_PATH = Path(BUILD_DIR).resolve()
29+
30+
# Security: block serving of certain sensitive files by extension/name
31+
FORBIDDEN_EXTENSIONS = {'.env', '.py', '.pem', '.key', '.db', '.sqlite', '.toml', '.ini'}
32+
FORBIDDEN_FILENAMES = {'Dockerfile', '.env', '.secrets', '.gitignore'}
33+
2634
# Serve static files from build directory
2735
app.mount(
2836
"/assets", StaticFiles(directory=os.path.join(BUILD_DIR, "assets")), name="assets"
2937
)
3038

3139

40+
@app.middleware("http")
41+
async def add_security_headers(request: Request, call_next):
42+
resp = await call_next(request)
43+
# Basic security headers; applications should extend CSP per app needs
44+
resp.headers.setdefault("X-Content-Type-Options", "nosniff")
45+
resp.headers.setdefault("X-Frame-Options", "DENY")
46+
resp.headers.setdefault("Referrer-Policy", "no-referrer")
47+
resp.headers.setdefault("Permissions-Policy", "geolocation=(), microphone=()")
48+
return resp
49+
50+
3251
@app.get("/")
3352
async def serve_index():
3453
return FileResponse(INDEX_HTML)
@@ -57,12 +76,57 @@ async def get_config():
5776

5877
@app.get("/{full_path:path}")
5978
async def serve_app(full_path: str):
60-
# First check if file exists in build directory
61-
file_path = os.path.join(BUILD_DIR, full_path)
62-
if os.path.exists(file_path):
63-
return FileResponse(file_path)
64-
# Otherwise serve index.html for client-side routing
65-
return FileResponse(INDEX_HTML)
79+
"""
80+
Safely serve static files from the build directory or return the SPA index.html.
81+
82+
Protections:
83+
- Prevent directory traversal by resolving candidate paths and ensuring they are inside BUILD_DIR.
84+
- Block dotfiles and sensitive extensions/names.
85+
- Return 404 on suspicious access instead of leaking details.
86+
"""
87+
try:
88+
candidate = (BUILD_DIR_PATH / full_path).resolve()
89+
90+
# Ensure resolved path is within BUILD_DIR
91+
if not str(candidate).startswith(str(BUILD_DIR_PATH)):
92+
raise HTTPException(status_code=404)
93+
94+
# Compute relative parts and block dotfiles anywhere in path
95+
try:
96+
rel_parts = candidate.relative_to(BUILD_DIR_PATH).parts
97+
except Exception:
98+
raise HTTPException(status_code=404)
99+
100+
if any(part.startswith('.') for part in rel_parts):
101+
raise HTTPException(status_code=404)
102+
103+
if candidate.name in FORBIDDEN_FILENAMES:
104+
raise HTTPException(status_code=404)
105+
106+
# If it's a regular file and allowed extension, serve it
107+
if candidate.is_file():
108+
if candidate.suffix.lower() in FORBIDDEN_EXTENSIONS:
109+
raise HTTPException(status_code=404)
110+
111+
headers = {
112+
"X-Content-Type-Options": "nosniff",
113+
"X-Frame-Options": "DENY",
114+
"Referrer-Policy": "no-referrer",
115+
}
116+
return FileResponse(str(candidate), headers=headers)
117+
118+
# Not a file -> fall back to SPA entrypoint
119+
return FileResponse(INDEX_HTML, headers={
120+
"X-Content-Type-Options": "nosniff",
121+
"X-Frame-Options": "DENY",
122+
"Referrer-Policy": "no-referrer",
123+
})
124+
125+
except HTTPException:
126+
raise
127+
except Exception:
128+
# Hide internal errors and respond with 404 to avoid information leakage
129+
raise HTTPException(status_code=404)
66130

67131

68132
if __name__ == "__main__":

0 commit comments

Comments
 (0)