Skip to content

Commit ff0847d

Browse files
path updated
1 parent 8145d53 commit ff0847d

File tree

1 file changed

+9
-72
lines changed

1 file changed

+9
-72
lines changed

src/frontend/frontend_server.py

Lines changed: 9 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
33

44
import uvicorn
55
from dotenv import load_dotenv
6-
from pathlib import Path
7-
from fastapi import FastAPI, HTTPException, Request
6+
from fastapi import FastAPI
87
from fastapi.middleware.cors import CORSMiddleware
98
from fastapi.responses import FileResponse, HTMLResponse
109
from fastapi.staticfiles import StaticFiles
@@ -25,30 +24,12 @@
2524
BUILD_DIR = os.path.join(os.path.dirname(__file__), "build")
2625
INDEX_HTML = os.path.join(BUILD_DIR, "index.html")
2726

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-
3527
# Serve static files from build directory
3628
app.mount(
3729
"/assets", StaticFiles(directory=os.path.join(BUILD_DIR, "assets")), name="assets"
3830
)
3931

4032

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-
5233
@app.get("/")
5334
async def serve_index():
5435
return FileResponse(INDEX_HTML)
@@ -69,58 +50,14 @@ async def get_config():
6950

7051
@app.get("/{full_path:path}")
7152
async def serve_app(full_path: str):
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-
# Normalize and join to avoid odd path segments, then resolve.
82-
# This mirrors the suggested remediation (normpath + join) but
83-
# uses Path.relative_to() as the final containment check.
84-
normalized = os.path.normpath(os.path.join(BUILD_DIR, full_path))
85-
candidate = Path(normalized).resolve()
86-
87-
try:
88-
rel_parts = candidate.relative_to(BUILD_DIR_PATH).parts
89-
except Exception:
90-
# Not contained -> possible traversal attempt
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)
123-
53+
# Remediation: normalize and check containment before serving
54+
file_path = os.path.normpath(os.path.join(BUILD_DIR, full_path))
55+
# Block traversal and dotfiles
56+
if not file_path.startswith(BUILD_DIR) or ".." in full_path or "/." in full_path or "\\." in full_path:
57+
return FileResponse(INDEX_HTML)
58+
if os.path.isfile(file_path):
59+
return FileResponse(file_path)
60+
return FileResponse(INDEX_HTML)
12461

12562
if __name__ == "__main__":
12663
uvicorn.run(app, host="127.0.0.1", port=3000)

0 commit comments

Comments
 (0)