Skip to content

Commit c4b370f

Browse files
Merge pull request #178 from microsoft/dev
fix: Information Disclosure - Path Traversal Security Vulnerability
2 parents 3a386db + a88b880 commit c4b370f

4 files changed

Lines changed: 42 additions & 16 deletions

File tree

infra/main.bicep

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1231,6 +1231,10 @@ module containerAppFrontend 'br/public:avm/res/app/container-app:0.18.1' = {
12311231
name: 'REACT_APP_MSAL_REDIRECT_URL'
12321232
value: '/'
12331233
}
1234+
{
1235+
name: 'ALLOWED_ORIGINS'
1236+
value: 'https://${frontEndContainerAppName}.${containerAppsEnvironment.outputs.defaultDomain}'
1237+
}
12341238
]
12351239
resources: {
12361240
cpu: '1'

infra/main.json

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"_generator": {
77
"name": "bicep",
88
"version": "0.41.2.15936",
9-
"templateHash": "8495628770560205121"
9+
"templateHash": "10582002328170601028"
1010
}
1111
},
1212
"parameters": {
@@ -26120,8 +26120,8 @@
2612026120
},
2612126121
"dependsOn": [
2612226122
"appIdentity",
26123-
"[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').storageQueue)]",
2612426123
"[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').storageBlob)]",
26124+
"[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').storageQueue)]",
2612526125
"virtualNetwork"
2612626126
]
2612726127
},
@@ -33808,9 +33808,9 @@
3380833808
},
3380933809
"dependsOn": [
3381033810
"aiFoundryAiServices",
33811+
"[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').cognitiveServices)]",
3381133812
"[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').aiServices)]",
3381233813
"[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').openAI)]",
33813-
"[format('avmPrivateDnsZones[{0}]', variables('dnsZoneIndex').cognitiveServices)]",
3381433814
"virtualNetwork"
3381533815
]
3381633816
},
@@ -40908,6 +40908,10 @@
4090840908
{
4090940909
"name": "REACT_APP_MSAL_REDIRECT_URL",
4091040910
"value": "/"
40911+
},
40912+
{
40913+
"name": "ALLOWED_ORIGINS",
40914+
"value": "[format('https://{0}.{1}', variables('frontEndContainerAppName'), reference('containerAppsEnvironment').outputs.defaultDomain.value)]"
4091140915
}
4091240916
],
4091340917
"resources": {

infra/main_custom.bicep

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1229,6 +1229,10 @@ module containerAppFrontend 'br/public:avm/res/app/container-app:0.18.1' = {
12291229
name: 'APP_ENV'
12301230
value: 'prod'
12311231
}
1232+
{
1233+
name: 'ALLOWED_ORIGINS'
1234+
value: 'https://${frontEndContainerAppName}.${containerAppsEnvironment.outputs.defaultDomain}'
1235+
}
12321236
]
12331237
resources: {
12341238
cpu: '1'

src/frontend/frontend_server.py

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,24 @@
22

33
import uvicorn
44
from dotenv import load_dotenv
5-
from fastapi import FastAPI
5+
from fastapi import FastAPI, Request
66
from fastapi.middleware.cors import CORSMiddleware
7-
from fastapi.responses import FileResponse, HTMLResponse
7+
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
88
from fastapi.staticfiles import StaticFiles
99

1010
# Load environment variables from .env file
1111
load_dotenv()
1212

1313
app = FastAPI()
1414

15+
# Read allowed origins from environment; fall back to same-origin only
16+
_allowed_origins = os.getenv("ALLOWED_ORIGINS", "").split(",")
17+
_allowed_origins = [o.strip() for o in _allowed_origins if o.strip()]
18+
1519
app.add_middleware(
1620
CORSMiddleware,
17-
allow_origins=["*"],
18-
allow_methods=["*"],
21+
allow_origins=_allowed_origins,
22+
allow_methods=["GET"],
1923
allow_headers=["*"],
2024
)
2125

@@ -35,20 +39,27 @@ async def serve_index():
3539

3640

3741
@app.get("/config")
38-
async def get_config():
42+
async def get_config(request: Request):
43+
# Only serve config to same-origin requests by checking the Referer/Origin
44+
origin = request.headers.get("origin") or ""
45+
referer = request.headers.get("referer") or ""
46+
host = request.headers.get("host") or ""
47+
if origin and not origin.endswith(host):
48+
return JSONResponse(status_code=403, content={"detail": "Forbidden"})
49+
3950
config = {
40-
"API_URL": os.getenv("API_URL", "API_URL not set"),
51+
"API_URL": os.getenv("API_URL", ""),
4152
"REACT_APP_MSAL_AUTH_CLIENTID": os.getenv(
42-
"REACT_APP_MSAL_AUTH_CLIENTID", "Client ID not set"
53+
"REACT_APP_MSAL_AUTH_CLIENTID", ""
4354
),
4455
"REACT_APP_MSAL_AUTH_AUTHORITY": os.getenv(
45-
"REACT_APP_MSAL_AUTH_AUTHORITY", "Authority not set"
56+
"REACT_APP_MSAL_AUTH_AUTHORITY", ""
4657
),
4758
"REACT_APP_MSAL_REDIRECT_URL": os.getenv(
48-
"REACT_APP_MSAL_REDIRECT_URL", "Redirect URL not set"
59+
"REACT_APP_MSAL_REDIRECT_URL", ""
4960
),
5061
"REACT_APP_MSAL_POST_REDIRECT_URL": os.getenv(
51-
"REACT_APP_MSAL_POST_REDIRECT_URL", "Post Redirect URL not set"
62+
"REACT_APP_MSAL_POST_REDIRECT_URL", ""
5263
),
5364
"REACT_APP_WEB_SCOPE": os.getenv(
5465
"REACT_APP_WEB_SCOPE", ""
@@ -63,9 +74,12 @@ async def get_config():
6374

6475
@app.get("/{full_path:path}")
6576
async def serve_app(full_path: str):
66-
# First check if file exists in build directory
67-
file_path = os.path.join(BUILD_DIR, full_path)
68-
if os.path.exists(file_path):
77+
# Resolve the requested path and ensure it stays within BUILD_DIR
78+
file_path = os.path.realpath(os.path.join(BUILD_DIR, full_path))
79+
build_dir_real = os.path.realpath(BUILD_DIR)
80+
if not file_path.startswith(build_dir_real + os.sep) and file_path != build_dir_real:
81+
return FileResponse(INDEX_HTML)
82+
if os.path.isfile(file_path):
6983
return FileResponse(file_path)
7084
# Otherwise serve index.html for client-side routing
7185
return FileResponse(INDEX_HTML)

0 commit comments

Comments
 (0)