Skip to content

fix: harden security — configurable CORS origin, role escalation, unauthenticated seed#26

Open
zikunz wants to merge 1 commit intoalphaonelabs:mainfrom
zikunz:fix/security-hardening
Open

fix: harden security — configurable CORS origin, role escalation, unauthenticated seed#26
zikunz wants to merge 1 commit intoalphaonelabs:mainfrom
zikunz:fix/security-hardening

Conversation

@zikunz
Copy link
Copy Markdown

@zikunz zikunz commented Mar 26, 2026

What

Three security fixes in src/worker.py:

  1. Configurable CORS originAccess-Control-Allow-Origin was hardcoded to *, allowing any website to read authenticated API responses (e.g. user dashboard data). The origin is now loaded from the ALLOWED_ORIGIN env var at runtime. When the variable is not set, the wildcard is preserved as a fallback so existing deployments are not disrupted.

  2. Self-assigned privileged rolesPOST /api/join accepted a user-supplied role field, allowing any authenticated user to enroll as instructor or organizer. The role is now hardcoded to participant.

  3. Unauthenticated /api/seed — Anyone could POST /api/seed without 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/init remains open because it only runs idempotent CREATE TABLE IF NOT EXISTS statements and is needed to bootstrap a fresh deployment before admin credentials exist.

Reproduction

All three issues were reproduced locally against wrangler dev on localhost:8787.

CORS origin:

curl -sI -X OPTIONS http://localhost:8787/api/activities -H "Origin: https://evil.com"
# Before: Access-Control-Allow-Origin: * (always)
# After:  Access-Control-Allow-Origin: <ALLOWED_ORIGIN value> (or * if not configured)

Role escalation:

# Register, then join as instructor
curl -s -X POST http://localhost:8787/api/join \
  -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
  -d '{"activity_id":"act-py-begin","role":"instructor"}'
# Before: dashboard shows Role: instructor
# After:  dashboard shows Role: participant

Unauthenticated seed:

curl -s -X POST http://localhost:8787/api/seed
# Before: {"success": true, "message": "Sample data seeded"}
# After:  401 Authentication required

Configuration

After this change, set ALLOWED_ORIGIN for cross-origin access:

  • Local dev: add ALLOWED_ORIGIN=http://localhost:8787 to .dev.vars
  • Production: wrangler secret put ALLOWED_ORIGIN

Same-origin requests (frontend served by the same worker) do not need CORS headers and work without this variable. When ALLOWED_ORIGIN is 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 the ALLOWED_ORIGIN environment variable, initialized lazily at runtime. Falls back to * when the variable is unset, maintaining backward compatibility for existing deployments.

Prevent Role Escalation

The /api/join endpoint no longer accepts a user-supplied role parameter. 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/seed endpoint now requires HTTP Basic Authentication using ADMIN_BASIC_USER and ADMIN_BASIC_PASS credentials, preventing unauthorized database overwrites with sample data. The /api/init endpoint 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.

…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.
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 26, 2026

Walkthrough

Modified CORS header initialization to lazily load from environment configuration. Removed request-supplied role handling in api_join, now always defaulting to "participant". Added basic authentication check for the POST /api/seed endpoint before database operations.

Changes

Cohort / File(s) Summary
CORS Origin Initialization
src/worker.py
Replaced unconditional wildcard CORS origin with lazy initialization using environment variable ALLOWED_ORIGIN, guarded by _CORS_ORIGIN_INITIALISED flag in _dispatch.
Request Handling Updates
src/worker.py
Removed role validation and parsing logic from api_join; role is now hardcoded to "participant" regardless of request input.
Seed Endpoint Security
src/worker.py
Added basic authentication requirement for POST /api/seed via _is_basic_auth_valid() check before database initialization proceeds.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly addresses the three main security improvements in the changeset: configurable CORS origin, role escalation prevention, and unauthenticated seed endpoint protection.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between a08bafc and 8008752.

📒 Files selected for processing (1)
  • src/worker.py

Comment on lines +1136 to +1141
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 "*"

Copy link
Copy Markdown

@coderabbitai coderabbitai bot Mar 26, 2026

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider avoiding global mutable state for CORS initialization.

The current approach works but has a few considerations worth noting:

  1. Static analysis flag (PLW0603): The global statement modifying _CORS_ORIGIN_INITIALISED is discouraged in Python best practices.
  2. One-shot initialization: The origin is read once per worker isolate lifetime. If env.ALLOWED_ORIGIN could vary (e.g., during hot config updates), this pattern won't pick up changes.
  3. 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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant