Skip to content

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

Open
zikunz wants to merge 3 commits intoalphaonelabs:mainfrom
zikunz:fix/security-hardening
Open

fix: harden security — configurable CORS origin, role escalation, unauthenticated seed#26
zikunz wants to merge 3 commits 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: Configurable CORS, Prevent Role Escalation, and Authenticated Seeding

This PR applies three targeted security fixes in src/worker.py to reduce attack surface and tighten access control.

  • Configurable CORS origin

    • Moves Access-Control-Allow-Origin out of the shared CORS template and initializes it once per worker isolate from ALLOWED_ORIGIN (trimmed). A per-isolate guard (_CORS_ORIGIN_INITIALISED) ensures one-time setup. Falls back to "*" when ALLOWED_ORIGIN is unset.
    • Documentation added for setting ALLOWED_ORIGIN in local (.dev.vars) and production (wrangler secret) environments.
    • Backward compatibility: fallback preserves prior "*" behavior when ALLOWED_ORIGIN is not set.
  • Prevent role escalation

    • POST /api/join ignores any client-supplied role; the enrolled role is unconditionally set to "participant" before insert/upsert, preventing authenticated users from self-assigning privileged roles (e.g., instructor, organizer).
  • Require auth for seeding

    • POST /api/seed now requires HTTP Basic Auth validated against ADMIN_BASIC_USER / ADMIN_BASIC_PASS; invalid or missing credentials return an unauthorized response. The change enforces that seeding cannot be triggered anonymously.
    • POST /api/init remains unauthenticated to allow idempotent DB bootstrapping (CREATE TABLE IF NOT EXISTS) before admin credentials exist.

Other notes

  • ALLOWED_ORIGIN is trimmed using the existing getattr(... or "").strip() pattern consistent with how admin creds are handled.
  • The changes are minimal and backward compatible for deployments that don't set ALLOWED_ORIGIN or admin credentials, but workflows relying on unauthenticated seeding or client-supplied roles must be updated.

Impact: strengthens authorization and access controls with small behavioral changes that prevent CORS misconfiguration, role escalation, and unauthenticated data seeding.

…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

Removed request-provided role handling in join requests (always set to "participant"), moved CORS origin initialization to per-worker lazy setup using ALLOWED_ORIGIN (guarded by _CORS_ORIGIN_INITIALISED), and added basic-auth enforcement for POST /api/seed before DB seeding.

Changes

Cohort / File(s) Summary
CORS Origin Initialization
src/worker.py
Removed unconditional Access-Control-Allow-Origin: * from shared _CORS; initialize origin once per worker in _dispatch from env.ALLOWED_ORIGIN (trimmed), guarded by _CORS_ORIGIN_INITIALISED.
Request Handling Updates
src/worker.py
api_join no longer parses or validates client body.role; it unconditionally sets role = "participant" before insert/upsert.
Seed Endpoint Security
src/worker.py
_dispatch now enforces Basic Auth for POST /api/seed via _is_basic_auth_valid(request, env) and returns _unauthorized_basic() on failure before calling init_db/seed_db.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 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 and clearly summarizes the three main security fixes: configurable CORS origin, role escalation prevention, and unauthenticated seed 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 thread src/worker.py
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR hardens security in the Cloudflare Worker API (src/worker.py) by tightening CORS behavior, preventing user-driven role escalation during enrollment, and protecting the database seeding endpoint with admin Basic Auth.

Changes:

  • Make Access-Control-Allow-Origin configurable via ALLOWED_ORIGIN (with a fallback behavior).
  • Force /api/join enrollments to always use the participant role.
  • Require admin HTTP Basic Auth for POST /api/seed.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/worker.py
Comment thread src/worker.py
Match the (getattr(...) or "").strip() pattern already used for
ADMIN_BASIC_USER and ADMIN_BASIC_PASS in _is_basic_auth_valid.
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`:
- Line 196: Add regression tests covering the three security-sensitive branches
once test infrastructure (e.g., pytest) is available: test the ALLOWED_ORIGIN
fallback and whitespace-stripping logic (use
CORS_ORIGIN_INITIALISED/ALLOWED_ORIGIN behavior), test that the /api/join
endpoint forces the participant role (assert the returned user/role is
participant even if request attempts to set another role), and test that
/api/seed requires valid basic auth (assert 401 when missing/invalid credentials
and success when valid). Implement focused unit/integration tests calling the
route handlers or test client to exercise these specific branches to prevent
regressions.
🪄 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: 53dfb936-f84c-4cb9-8146-b5ee24362621

📥 Commits

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

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

Comment thread src/worker.py
@zikunz
Copy link
Copy Markdown
Author

zikunz commented Apr 12, 2026

@A1L13N All review feedback has been addressed and resolved. Would appreciate your review when you get a chance.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/worker.py (1)

1364-1373: 🧹 Nitpick | 🔵 Trivial

Nice gating of /api/seed — and good call leaving /api/init open. 🔐

Requiring Basic Auth before init_db + seed_db closes the unauthenticated-overwrite hole cleanly, and _is_basic_auth_valid already does the right things (rejects when ADMIN_BASIC_USER/ADMIN_BASIC_PASS are unset or blank, uses hmac.compare_digest for timing-safe comparison, decodes carefully inside a try/except). The reasoning in the PR description for keeping /api/init unauthenticated — idempotent CREATE TABLE IF NOT EXISTS only, needed to bootstrap before admin creds exist — is sound.

One small operational thought for the future (not blocking): once you have structured logging in place, it'd be worth emitting a warn-level record on failed /api/seed auth attempts so abuse is visible in whatever log sink you're using. Totally fine as a follow-up.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/worker.py` around lines 1364 - 1373, Add a warn-level log when a client
fails Basic Auth on the /api/seed path: inside the handler where you call
_is_basic_auth_valid(request, env) and you currently return
_unauthorized_basic(), emit a warning (e.g., via the app/logger available as
env.logger or similar) recording the endpoint "/api/seed", the fact that auth
failed, and minimal request context (e.g., remote IP or headers) before
returning; keep the existing flow and do not log sensitive secrets, and leave
init_db, seed_db, capture_exception, and _unauthorized_basic unchanged.
🤖 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 1063-1068: Update the tests in tests/test_api_join_dashboard.py to
assert that the persisted enrollment row uses role "participant" regardless of
client input: change the existing test_invalid_role_defaults_to_participant and
test_valid_role_instructor to call worker.api_join (or the helper _req) with a
client-supplied "role" (e.g., "instructor"), then capture the INSERT statement
(or insert_stmt.bound_args) and assert its role parameter equals "participant";
keep the request/status checks but add the DB-bound-parameter assertion so
api_join (which hardcodes role = "participant" in worker.py) is explicitly
verified rather than relying on a 200 status alone.

---

Outside diff comments:
In `@src/worker.py`:
- Around line 1364-1373: Add a warn-level log when a client fails Basic Auth on
the /api/seed path: inside the handler where you call
_is_basic_auth_valid(request, env) and you currently return
_unauthorized_basic(), emit a warning (e.g., via the app/logger available as
env.logger or similar) recording the endpoint "/api/seed", the fact that auth
failed, and minimal request context (e.g., remote IP or headers) before
returning; keep the existing flow and do not log sensitive secrets, and leave
init_db, seed_db, capture_exception, and _unauthorized_basic unchanged.
🪄 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: f7e97f70-94fd-4bb3-9742-e0c9796670c4

📥 Commits

Reviewing files that changed from the base of the PR and between 94b21f3 and a9fd543.

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

Comment thread src/worker.py
Comment on lines 1063 to +1068
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"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Privilege-escalation fix looks solid 🛡️ — please refresh the stale join tests.

Hardcoding role = "participant" and dropping any client-supplied value is exactly the right move here; an authenticated user can no longer self-promote to instructor/organizer via the join endpoint.

One downstream cleanup: the existing tests in tests/test_api_join_dashboard.py (test_invalid_role_defaults_to_participant and test_valid_role_instructor) still POST a role field and only assert status == 200. After this change they pass vacuously — the request body's role is now silently ignored, so neither test actually verifies any role-selection behavior, and test_valid_role_instructor is now misleadingly named (an instructor role can no longer be obtained through /api/join). Per the repo's Python guideline to "verify tests cover the key logic paths," it'd be worth tightening these to assert the persisted enrollment row's role is "participant" regardless of what the client sent — that way the test suite actively guards the new invariant instead of rubber-stamping it.

🧪 Sketch of the hardened assertion
async def test_join_ignores_client_supplied_role(self):
    tok = _token()
    act_row = MockRow(id="act-1")
    insert_stmt = make_stmt()  # capture bound params
    env = make_env(db=MockDB([make_stmt(first=act_row), insert_stmt]))
    r = await worker.api_join(
        self._req({"activity_id": "act-1", "role": "instructor"}, token=tok), env
    )
    assert r.status == 200
    # The 4th bound parameter to the INSERT is the role column.
    assert insert_stmt.bound_args[-1] == "participant"

As per coding guidelines: "Verify tests cover the key logic paths."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/worker.py` around lines 1063 - 1068, Update the tests in
tests/test_api_join_dashboard.py to assert that the persisted enrollment row
uses role "participant" regardless of client input: change the existing
test_invalid_role_defaults_to_participant and test_valid_role_instructor to call
worker.api_join (or the helper _req) with a client-supplied "role" (e.g.,
"instructor"), then capture the INSERT statement (or insert_stmt.bound_args) and
assert its role parameter equals "participant"; keep the request/status checks
but add the DB-bound-parameter assertion so api_join (which hardcodes role =
"participant" in worker.py) is explicitly verified rather than relying on a 200
status alone.

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.

2 participants