fix: harden security — configurable CORS origin, role escalation, unauthenticated seed#26
fix: harden security — configurable CORS origin, role escalation, unauthenticated seed#26zikunz wants to merge 1 commit intoalphaonelabs:mainfrom
Conversation
…uthenticated seed 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.
WalkthroughModified CORS header initialization to lazily load from environment configuration. Removed request-supplied role handling in Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes 🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/worker.py`:
- Around line 1136-1141: The module currently uses a global mutable flag
_CORS_ORIGIN_INITIALISED and mutates the shared _CORS dict during import-time
which triggers the PLW0603 warning and prevents picking up runtime changes to
env.ALLOWED_ORIGIN; replace this by adding a small helper function
_cors_headers(env) that returns a new headers dict (copying existing _CORS base
entries and setting "Access-Control-Allow-Origin" to env.ALLOWED_ORIGIN or "*"
if empty) and update all response helpers (json_resp, _unauthorized_basic,
serve_static) to call _cors_headers(env) per-request instead of relying on
_CORS/_CORS_ORIGIN_INITIALISED so there is no global state mutation and hot
config changes are honored.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository: alphaonelabs/coderabbit/.coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: b4fa9601-e34d-4905-8f69-3a174425d8d7
📒 Files selected for processing (1)
src/worker.py
| 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 "*" | ||
|
|
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider avoiding global mutable state for CORS initialization.
The current approach works but has a few considerations worth noting:
- Static analysis flag (PLW0603): The
globalstatement modifying_CORS_ORIGIN_INITIALISEDis discouraged in Python best practices. - One-shot initialization: The origin is read once per worker isolate lifetime. If
env.ALLOWED_ORIGINcould vary (e.g., during hot config updates), this pattern won't pick up changes. - Dict mutation timing: Concurrent requests could theoretically both enter the initialization block before the flag is set, though in practice this is benign since both would write the same value.
A cleaner approach would be to compute the headers at response time or use a helper that returns the complete headers dict:
♻️ Suggested refactor: Compute CORS headers per-request
-_CORS_ORIGIN_INITIALISED = False
_CORS = {
+ "Access-Control-Allow-Origin": "*", # default, overridden by _cors_headers()
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
}
+
+def _cors_headers(env) -> dict:
+ """Return CORS headers with origin from env (or fallback to *)."""
+ origin = getattr(env, "ALLOWED_ORIGIN", "") or "*"
+ return {**_CORS, "Access-Control-Allow-Origin": origin}Then update response helpers (json_resp, _unauthorized_basic, serve_static) to accept env and use _cors_headers(env).
That said, if simplicity and minimal code churn are priorities, the current implementation is functional and achieves the security goal. The fallback to "*" is documented and intentional per the PR objectives.
🧰 Tools
🪛 Ruff (0.15.7)
[warning] 1136-1136: Using the global statement to update _CORS_ORIGIN_INITIALISED is discouraged
(PLW0603)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/worker.py` around lines 1136 - 1141, The module currently uses a global
mutable flag _CORS_ORIGIN_INITIALISED and mutates the shared _CORS dict during
import-time which triggers the PLW0603 warning and prevents picking up runtime
changes to env.ALLOWED_ORIGIN; replace this by adding a small helper function
_cors_headers(env) that returns a new headers dict (copying existing _CORS base
entries and setting "Access-Control-Allow-Origin" to env.ALLOWED_ORIGIN or "*"
if empty) and update all response helpers (json_resp, _unauthorized_basic,
serve_static) to call _cors_headers(env) per-request instead of relying on
_CORS/_CORS_ORIGIN_INITIALISED so there is no global state mutation and hot
config changes are honored.
There was a problem hiding this comment.
Thanks for the suggestion. I considered the per-request approach but it would require threading env through json_resp, ok, err, _unauthorized_basic, and serve_static, touching many call sites. For a focused security fix, I preferred minimal churn. I am happy to refactor in a follow-up if the maintainers prefer that pattern.
There was a problem hiding this comment.
@zikunz — That's a completely reasonable trade-off. Keeping the diff focused on the security fix rather than introducing a broad refactor across multiple helpers is a sound engineering decision. The current implementation is functional and achieves the intended goal.
If you'd like, I can open a follow-up issue to track the per-request CORS header refactor so it doesn't get lost. Just let me know!
What
Three security fixes in
src/worker.py:Configurable CORS origin —
Access-Control-Allow-Originwas hardcoded to*, allowing any website to read authenticated API responses (e.g. user dashboard data). The origin is now loaded from theALLOWED_ORIGINenv var at runtime. When the variable is not set, the wildcard is preserved as a fallback so existing deployments are not disrupted.Self-assigned privileged roles —
POST /api/joinaccepted a user-suppliedrolefield, allowing any authenticated user to enroll asinstructorororganizer. The role is now hardcoded toparticipant.Unauthenticated
/api/seed— Anyone couldPOST /api/seedwithout credentials and overwrite the database with sample data. The endpoint now requires the same HTTP Basic Auth used by the admin panel (ADMIN_BASIC_USER/ADMIN_BASIC_PASS)./api/initremains open because it only runs idempotentCREATE TABLE IF NOT EXISTSstatements and is needed to bootstrap a fresh deployment before admin credentials exist.Reproduction
All three issues were reproduced locally against
wrangler devonlocalhost:8787.CORS origin:
Role escalation:
Unauthenticated seed:
Configuration
After this change, set
ALLOWED_ORIGINfor cross-origin access:ALLOWED_ORIGIN=http://localhost:8787to.dev.varswrangler secret put ALLOWED_ORIGINSame-origin requests (frontend served by the same worker) do not need CORS headers and work without this variable. When
ALLOWED_ORIGINis not configured, the wildcard*is used as a fallback to avoid breaking existing deployments.Security Hardening: CORS, Role Escalation, and Unauthenticated Seed Protection
This PR implements three security improvements to the API:
Configurable CORS Origin
The hardcoded
Access-Control-Allow-Origin: *header is now configured via theALLOWED_ORIGINenvironment variable, initialized lazily at runtime. Falls back to*when the variable is unset, maintaining backward compatibility for existing deployments.Prevent Role Escalation
The
/api/joinendpoint no longer accepts a user-suppliedroleparameter. User enrollment is now hardcoded to the"participant"role, preventing authenticated users from self-assigning privileged roles like"instructor"or"organizer".Require Authentication for Seed Endpoint
The
/api/seedendpoint now requires HTTP Basic Authentication usingADMIN_BASIC_USERandADMIN_BASIC_PASScredentials, preventing unauthorized database overwrites with sample data. The/api/initendpoint remains unauthenticated to support bootstrapping fresh deployments before admin credentials are configured.Impact: These changes strengthen authorization and access control while maintaining backward compatibility for deployments that don't configure the new environment variables. Configuration guidance is provided for development and production environments.