From 3a0ec34a519e07d8f05a67acd46bc59ee5f7d6b2 Mon Sep 17 00:00:00 2001 From: Prajwal D C Date: Mon, 6 Apr 2026 12:08:21 +0530 Subject: [PATCH 1/3] fix: patch path traversal (CWE-22) and harden /config endpoint [MSRC 112301] - Use os.path.realpath() to resolve symlinks and '..' sequences, then verify the resolved path stays within BUILD_DIR before serving files. - Replace os.path.exists() with os.path.isfile() to prevent directory listing. - Replace allow_origins=['*'] CORS wildcard with configurable ALLOWED_ORIGINS env var (empty = same-origin only). - Restrict CORS allow_methods to GET. - Add origin-vs-host check on /config to block cross-origin reads. - Remove verbose fallback strings from /config defaults that disclosed deployment state. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/frontend/frontend_server.py | 40 ++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/src/frontend/frontend_server.py b/src/frontend/frontend_server.py index 71ea62b..1ab61f5 100644 --- a/src/frontend/frontend_server.py +++ b/src/frontend/frontend_server.py @@ -2,9 +2,9 @@ import uvicorn from dotenv import load_dotenv -from fastapi import FastAPI +from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import FileResponse, HTMLResponse +from fastapi.responses import FileResponse, HTMLResponse, JSONResponse from fastapi.staticfiles import StaticFiles # Load environment variables from .env file @@ -12,10 +12,14 @@ app = FastAPI() +# Read allowed origins from environment; fall back to same-origin only +_allowed_origins = os.getenv("ALLOWED_ORIGINS", "").split(",") +_allowed_origins = [o.strip() for o in _allowed_origins if o.strip()] + app.add_middleware( CORSMiddleware, - allow_origins=["*"], - allow_methods=["*"], + allow_origins=_allowed_origins, + allow_methods=["GET"], allow_headers=["*"], ) @@ -35,20 +39,27 @@ async def serve_index(): @app.get("/config") -async def get_config(): +async def get_config(request: Request): + # Only serve config to same-origin requests by checking the Referer/Origin + origin = request.headers.get("origin") or "" + referer = request.headers.get("referer") or "" + host = request.headers.get("host") or "" + if origin and not origin.endswith(host): + return JSONResponse(status_code=403, content={"detail": "Forbidden"}) + config = { - "API_URL": os.getenv("API_URL", "API_URL not set"), + "API_URL": os.getenv("API_URL", ""), "REACT_APP_MSAL_AUTH_CLIENTID": os.getenv( - "REACT_APP_MSAL_AUTH_CLIENTID", "Client ID not set" + "REACT_APP_MSAL_AUTH_CLIENTID", "" ), "REACT_APP_MSAL_AUTH_AUTHORITY": os.getenv( - "REACT_APP_MSAL_AUTH_AUTHORITY", "Authority not set" + "REACT_APP_MSAL_AUTH_AUTHORITY", "" ), "REACT_APP_MSAL_REDIRECT_URL": os.getenv( - "REACT_APP_MSAL_REDIRECT_URL", "Redirect URL not set" + "REACT_APP_MSAL_REDIRECT_URL", "" ), "REACT_APP_MSAL_POST_REDIRECT_URL": os.getenv( - "REACT_APP_MSAL_POST_REDIRECT_URL", "Post Redirect URL not set" + "REACT_APP_MSAL_POST_REDIRECT_URL", "" ), "REACT_APP_WEB_SCOPE": os.getenv( "REACT_APP_WEB_SCOPE", "" @@ -63,9 +74,12 @@ async def get_config(): @app.get("/{full_path:path}") async def serve_app(full_path: str): - # First check if file exists in build directory - file_path = os.path.join(BUILD_DIR, full_path) - if os.path.exists(file_path): + # Resolve the requested path and ensure it stays within BUILD_DIR + file_path = os.path.realpath(os.path.join(BUILD_DIR, full_path)) + build_dir_real = os.path.realpath(BUILD_DIR) + if not file_path.startswith(build_dir_real + os.sep) and file_path != build_dir_real: + return FileResponse(INDEX_HTML) + if os.path.isfile(file_path): return FileResponse(file_path) # Otherwise serve index.html for client-side routing return FileResponse(INDEX_HTML) From c38520ba32d524b57d85d5ba1fdf4dae34353094 Mon Sep 17 00:00:00 2001 From: Prajwal D C Date: Mon, 6 Apr 2026 12:26:57 +0530 Subject: [PATCH 2/3] infra: set ALLOWED_ORIGINS env var for frontend container app Add ALLOWED_ORIGINS to both main.bicep and main_custom.bicep so the frontend CORS policy is automatically configured with the container app's own FQDN during Azure deployment. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- infra/main.bicep | 4 ++++ infra/main_custom.bicep | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/infra/main.bicep b/infra/main.bicep index 8f9cbd3..0aa36c8 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -1231,6 +1231,10 @@ module containerAppFrontend 'br/public:avm/res/app/container-app:0.18.1' = { name: 'REACT_APP_MSAL_REDIRECT_URL' value: '/' } + { + name: 'ALLOWED_ORIGINS' + value: 'https://${frontEndContainerAppName}.${containerAppsEnvironment.outputs.defaultDomain}' + } ] resources: { cpu: '1' diff --git a/infra/main_custom.bicep b/infra/main_custom.bicep index 9b63e7c..137f5b0 100644 --- a/infra/main_custom.bicep +++ b/infra/main_custom.bicep @@ -1229,6 +1229,10 @@ module containerAppFrontend 'br/public:avm/res/app/container-app:0.18.1' = { name: 'APP_ENV' value: 'prod' } + { + name: 'ALLOWED_ORIGINS' + value: 'https://${frontEndContainerAppName}.${containerAppsEnvironment.outputs.defaultDomain}' + } ] resources: { cpu: '1' From 0a7bd20f61c5a4a720dd8dcf4c9a31a5f0c61f40 Mon Sep 17 00:00:00 2001 From: Prajwal D C Date: Mon, 6 Apr 2026 15:55:55 +0530 Subject: [PATCH 3/3] build: rebuild main.json from updated main.bicep Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- infra/main.json | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/infra/main.json b/infra/main.json index b11068a..97bddf0 100644 --- a/infra/main.json +++ b/infra/main.json @@ -6,7 +6,7 @@ "_generator": { "name": "bicep", "version": "0.41.2.15936", - "templateHash": "8495628770560205121" + "templateHash": "10582002328170601028" } }, "parameters": { @@ -26120,8 +26120,8 @@ }, "dependsOn": [ "appIdentity", - "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').storageQueue)]", "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').storageBlob)]", + "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').storageQueue)]", "virtualNetwork" ] }, @@ -33808,9 +33808,9 @@ }, "dependsOn": [ "aiFoundryAiServices", + "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').cognitiveServices)]", "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').aiServices)]", "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').openAI)]", - "[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').cognitiveServices)]", "virtualNetwork" ] }, @@ -40908,6 +40908,10 @@ { "name": "REACT_APP_MSAL_REDIRECT_URL", "value": "/" + }, + { + "name": "ALLOWED_ORIGINS", + "value": "[format('https://{0}.{1}', variables('frontEndContainerAppName'), reference('containerAppsEnvironment').outputs.defaultDomain.value)]" } ], "resources": {