From 800875283b814d5c812debb9857e6ed4462504b8 Mon Sep 17 00:00:00 2001 From: Zikun Zhu Date: Fri, 27 Mar 2026 03:49:36 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20harden=20security=20=E2=80=94=20configur?= =?UTF-8?q?able=20CORS=20origin,=20role=20escalation,=20unauthenticated=20?= =?UTF-8?q?seed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Read the Access-Control-Allow-Origin value from the ALLOWED_ORIGIN environment variable at runtime instead of hardcoding the wildcard (*). When the variable is not configured the wildcard is preserved as a fallback so existing deployments are not disrupted. Force the enrollment role to "participant" in api_join. Previously a user could self-assign "instructor" or "organizer" by including a role field in the request body. Require HTTP Basic Auth on the /api/seed endpoint to prevent unauthenticated callers from overwriting the database with sample data. /api/init is left open because it is idempotent (CREATE TABLE IF NOT EXISTS) and is needed for first-time bootstrap before admin credentials are configured. --- src/worker.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/worker.py b/src/worker.py index 9656277..38bb654 100644 --- a/src/worker.py +++ b/src/worker.py @@ -193,8 +193,8 @@ def verify_token(raw: str, secret: str): # Response helpers # --------------------------------------------------------------------------- +_CORS_ORIGIN_INITIALISED = False _CORS = { - "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", "Access-Control-Allow-Headers": "Content-Type, Authorization", } @@ -870,12 +870,11 @@ async def api_join(req, env): return bad_resp act_id = body.get("activity_id") - role = (body.get("role") or "participant").strip() if not act_id: return err("activity_id is required") - if role not in ("participant", "instructor", "organizer"): - role = "participant" + + role = "participant" act = await env.DB.prepare( "SELECT id FROM activities WHERE id=?" @@ -1134,6 +1133,12 @@ async def serve_static(path: str, env): # --------------------------------------------------------------------------- async def _dispatch(request, env): + global _CORS_ORIGIN_INITIALISED + if not _CORS_ORIGIN_INITIALISED: + _CORS_ORIGIN_INITIALISED = True + origin = getattr(env, "ALLOWED_ORIGIN", "") + _CORS["Access-Control-Allow-Origin"] = origin if origin else "*" + path = urlparse(request.url).path method = request.method.upper() admin_path = _clean_path(getattr(env, "ADMIN_URL", "")) @@ -1156,6 +1161,8 @@ async def _dispatch(request, env): return err("Database init failed — check D1 binding", 500) if path == "/api/seed" and method == "POST": + if not _is_basic_auth_valid(request, env): + return _unauthorized_basic() try: await init_db(env) await seed_db(env, env.ENCRYPTION_KEY)