Skip to content

Commit 6ae0800

Browse files
sfi issue fix
1 parent ffffd12 commit 6ae0800

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
@@ -3,7 +3,8 @@
33

44
import uvicorn
55
from dotenv import load_dotenv
6-
from fastapi import FastAPI
6+
from pathlib import Path
7+
from fastapi import FastAPI, HTTPException, Request
78
from fastapi.middleware.cors import CORSMiddleware
89
from fastapi.responses import FileResponse, HTMLResponse
910
from fastapi.staticfiles import StaticFiles
@@ -24,12 +25,30 @@
2425
BUILD_DIR = os.path.join(os.path.dirname(__file__), "build")
2526
INDEX_HTML = os.path.join(BUILD_DIR, "index.html")
2627

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

3240

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

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

60124

61125
if __name__ == "__main__":

0 commit comments

Comments
 (0)