Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions infra/main.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
10 changes: 7 additions & 3 deletions infra/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"_generator": {
"name": "bicep",
"version": "0.41.2.15936",
"templateHash": "8495628770560205121"
"templateHash": "10582002328170601028"
}
},
"parameters": {
Expand Down Expand Up @@ -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"
]
},
Expand Down Expand Up @@ -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"
]
},
Expand Down Expand Up @@ -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": {
Expand Down
4 changes: 4 additions & 0 deletions infra/main_custom.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
40 changes: 27 additions & 13 deletions src/frontend/frontend_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,24 @@

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

Copilot AI Apr 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HTMLResponse is imported but no longer used in this module. Removing unused imports helps keep the file lint-clean and avoids confusion about supported response types.

Suggested change
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
from fastapi.responses import FileResponse, JSONResponse

Copilot uses AI. Check for mistakes.
from fastapi.staticfiles import StaticFiles

# Load environment variables from .env file
load_dotenv()

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=["*"],
)

Expand All @@ -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"})
Comment on lines +43 to +48

Copilot AI Apr 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same-origin check is currently origin.endswith(host), which is vulnerable to suffix-matching bypasses (e.g., https://notexample.com endswith example.com) and can also misbehave with ports/IDNs. Consider parsing the Origin/Referer URL and comparing the hostname (and port if present) for exact equality against the expected host (or against the configured ALLOWED_ORIGINS list), and remove referer if it’s not used.

Copilot uses AI. Check for mistakes.

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", ""
Expand All @@ -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)
Expand Down
Loading