Skip to content

Commit f18e98e

Browse files
SFI issue fix
1 parent 1c117ef commit f18e98e

File tree

1 file changed

+9
-70
lines changed

1 file changed

+9
-70
lines changed

src/frontend/frontend_server.py

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

33
import uvicorn
44
from dotenv import load_dotenv
5-
from pathlib import Path
6-
from fastapi import FastAPI, HTTPException, Request
5+
from fastapi import FastAPI
76
from fastapi.middleware.cors import CORSMiddleware
87
from fastapi.responses import FileResponse, HTMLResponse
98
from fastapi.staticfiles import StaticFiles
@@ -24,30 +23,13 @@
2423
BUILD_DIR = os.path.join(os.path.dirname(__file__), "dist")
2524
INDEX_HTML = os.path.join(BUILD_DIR, "index.html")
2625

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'}
3326

3427
# Serve static files from build directory
3528
app.mount(
3629
"/assets", StaticFiles(directory=os.path.join(BUILD_DIR, "assets")), name="assets"
3730
)
3831

3932

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-
5133
@app.get("/")
5234
async def serve_index():
5335
return FileResponse(INDEX_HTML)
@@ -76,57 +58,14 @@ async def get_config():
7658

7759
@app.get("/{full_path:path}")
7860
async def serve_app(full_path: str):
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)
61+
# Remediation: normalize and check containment before serving
62+
file_path = os.path.normpath(os.path.join(BUILD_DIR, full_path))
63+
# Block traversal and dotfiles
64+
if not file_path.startswith(BUILD_DIR) or ".." in full_path or "/." in full_path or "\\." in full_path:
65+
return FileResponse(INDEX_HTML)
66+
if os.path.isfile(file_path):
67+
return FileResponse(file_path)
68+
return FileResponse(INDEX_HTML)
13069

13170

13271
if __name__ == "__main__":

0 commit comments

Comments
 (0)