Skip to content
Open
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
30 changes: 29 additions & 1 deletion gitd/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"""

import logging
import os
from contextlib import asynccontextmanager

from fastapi import FastAPI
Expand All @@ -27,6 +28,29 @@

logger = logging.getLogger(__name__)

# Default CORS allowlist — the local dashboard (Vite dev server, port 6175)
# and the local docs site (Astro/Starlight, port 4321). Anything else is
# refused so a random web page opened in the developer's browser cannot
# talk to the API (which exposes ADB, skill install, file I/O, etc.).
# Additional origins can be supplied via the GITD_CORS_ORIGINS env var
# (comma-separated).
_DEFAULT_CORS_ORIGINS = (
"http://localhost:6175",
"http://127.0.0.1:6175",
"http://localhost:5173",
"http://127.0.0.1:5173",
"http://localhost:4321",
"http://127.0.0.1:4321",
)


def _cors_origins() -> list[str]:
"""Return the list of allowed CORS origins, honoring GITD_CORS_ORIGINS."""
override = os.environ.get("GITD_CORS_ORIGINS", "").strip()
if not override:
return list(_DEFAULT_CORS_ORIGINS)
return [o.strip() for o in override.split(",") if o.strip()]


@asynccontextmanager
async def lifespan(app: FastAPI):
Expand Down Expand Up @@ -78,9 +102,13 @@ def create_app() -> FastAPI:
openapi_tags=TAGS_METADATA,
)

# CORS: restrict to the local dashboard/docs origins by default. A
# wildcard combined with allow_credentials=True lets *any* website the
# developer visits issue credentialed requests to this API, which in
# turn can drive ADB (/api/phone/*), install skills, etc. See CWE-352.
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_origins=_cors_origins(),
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
Expand Down
39 changes: 39 additions & 0 deletions tests/api/test_cors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""CORS policy regression tests — see CWE-352 fix.

The API exposes sensitive tools (ADB, skill install, file I/O). A wildcard
CORS allowlist paired with `allow_credentials=True` lets any web page the
developer visits issue credentialed requests to the API. Guard against
regressions by asserting the middleware only trusts the local dashboard/docs
origins.
"""
from fastapi.testclient import TestClient

from gitd.app import app


def test_cors_rejects_arbitrary_origin():
with TestClient(app) as c:
r = c.options(
"/api/health",
headers={
"Origin": "https://evil.example",
"Access-Control-Request-Method": "GET",
},
)
acao = r.headers.get("access-control-allow-origin", "")
assert acao != "https://evil.example", (
"CORS must not echo arbitrary Origin — see CWE-352 fix in gitd/app.py"
)
assert acao != "*", "wildcard ACAO with credentials is unsafe"


def test_cors_allows_frontend_dev_origin():
with TestClient(app) as c:
r = c.options(
"/api/health",
headers={
"Origin": "http://localhost:6175",
"Access-Control-Request-Method": "GET",
},
)
assert r.headers.get("access-control-allow-origin") == "http://localhost:6175"