🛡️ Sentinel: [CRITICAL] Fix missing auth on migrate-formats#14
🛡️ Sentinel: [CRITICAL] Fix missing auth on migrate-formats#14
Conversation
🚨 Severity: CRITICAL 💡 Vulnerability: The `/auth/migrate-formats` endpoint was publicly accessible without authentication. 🔧 Fix: Added `X-Admin-Secret` header validation using `secrets.compare_digest`. ✅ Verification: Added backend tests verifying 403/200 responses. Co-authored-by: Horus0305 <98160215+Horus0305@users.noreply.github.com>
|
👋 Jules, reporting for duty! I'm here to lend a hand with this pull request. When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down. I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job! For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with New to Jules? Learn more at jules.google/docs. For security, I will only act on instructions from the user who triggered this task. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
This PR aims to remediate a critical security issue by restricting the /auth/migrate-formats data-migration endpoint to callers who provide an admin secret, preventing unauthenticated users from triggering DB-modifying migrations.
Changes:
- Added
ADMIN_SECRETconfiguration inbackend/core/config.py. - Updated
/auth/migrate-formatsto requireX-Admin-Secretand validate it usingsecrets.compare_digest. - Added an auth test module for the new admin-secret requirement (and introduced a committed SQLite DB artifact).
Reviewed changes
Copilot reviewed 3 out of 4 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
cricket.db |
Adds a full SQLite database file to the repo (problematic artifact; not required for the auth fix). |
CricketGame/backend/test_auth.py |
Adds tests for the admin-secret behavior, but currently introduces import-time side effects and doesn’t match existing test execution conventions. |
CricketGame/backend/core/config.py |
Introduces ADMIN_SECRET env config, but currently includes a hardcoded fallback secret in non-prod. |
CricketGame/backend/api/auth.py |
Requires X-Admin-Secret on /auth/migrate-formats and rejects invalid secrets with 403. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| from fastapi.testclient import TestClient | ||
| from backend.main import app | ||
| from backend.core.config import ADMIN_SECRET | ||
| from backend.data.database import init_db | ||
|
|
||
| # Initialize DB for tests | ||
| init_db() | ||
|
|
||
| client = TestClient(app) | ||
|
|
||
| def test_migrate_formats_no_header(): | ||
| # Expect 422 because the header is required (Header(...)) | ||
| response = client.get("/auth/migrate-formats") | ||
| assert response.status_code == 422 | ||
|
|
||
| def test_migrate_formats_wrong_header(): | ||
| # Expect 403 because the secret is invalid | ||
| response = client.get("/auth/migrate-formats", headers={"X-Admin-Secret": "wrong-secret"}) | ||
| assert response.status_code == 403 | ||
| assert response.json()["detail"] == "Invalid admin secret" | ||
|
|
||
| def test_migrate_formats_correct_header(): |
There was a problem hiding this comment.
init_db() is executed at import time and TestClient(app) is created as a module-level global. With this app, constructing the TestClient will run the FastAPI lifespan (which starts the background learning processor) and init_db() will write to the real sqlite:///./cricket.db by default. This can create/modify local files during test import and make tests flaky. Prefer creating the client inside each test (or a fixture), and configure an isolated test database (e.g., set DATABASE_URL to an in-memory/temporary SQLite URL before importing the app).
| from fastapi.testclient import TestClient | |
| from backend.main import app | |
| from backend.core.config import ADMIN_SECRET | |
| from backend.data.database import init_db | |
| # Initialize DB for tests | |
| init_db() | |
| client = TestClient(app) | |
| def test_migrate_formats_no_header(): | |
| # Expect 422 because the header is required (Header(...)) | |
| response = client.get("/auth/migrate-formats") | |
| assert response.status_code == 422 | |
| def test_migrate_formats_wrong_header(): | |
| # Expect 403 because the secret is invalid | |
| response = client.get("/auth/migrate-formats", headers={"X-Admin-Secret": "wrong-secret"}) | |
| assert response.status_code == 403 | |
| assert response.json()["detail"] == "Invalid admin secret" | |
| def test_migrate_formats_correct_header(): | |
| import os | |
| import pytest | |
| from fastapi.testclient import TestClient | |
| # Configure an isolated test database before importing the app | |
| os.environ.setdefault("DATABASE_URL", "sqlite:///:memory:") | |
| from backend.main import app | |
| from backend.core.config import ADMIN_SECRET | |
| from backend.data.database import init_db | |
| @pytest.fixture | |
| def client(): | |
| # Initialize DB for each test using the test database | |
| init_db() | |
| with TestClient(app) as test_client: | |
| yield test_client | |
| def test_migrate_formats_no_header(client): | |
| # Expect 422 because the header is required (Header(...)) | |
| response = client.get("/auth/migrate-formats") | |
| assert response.status_code == 422 | |
| def test_migrate_formats_wrong_header(client): | |
| # Expect 403 because the secret is invalid | |
| response = client.get("/auth/migrate-formats", headers={"X-Admin-Secret": "wrong-secret"}) | |
| assert response.status_code == 403 | |
| assert response.json()["detail"] == "Invalid admin secret" | |
| def test_migrate_formats_correct_header(client): |
| data = response.json() | ||
| assert "format_stats_2v2_merged" in data | ||
| assert "duplicate_rows_removed" in data | ||
| assert "match_history_fixed" in data |
There was a problem hiding this comment.
This test module is written in pytest style, but the existing backend tests in this repo are standalone scripts (with run_all_tests() + __main__) and requirements.txt does not include pytest. Unless the project is switching to pytest (and adding it to dependencies/CI), these tests likely won't run in the current test workflow. Either adapt this test to the existing script pattern, or add pytest as a dependency and update the repo’s test instructions accordingly.
| assert "match_history_fixed" in data | |
| assert "match_history_fixed" in data | |
| def run_all_tests(): | |
| """ | |
| Run all tests in this module. | |
| This mirrors the standalone script pattern used by other backend tests. | |
| """ | |
| test_migrate_formats_no_header() | |
| test_migrate_formats_wrong_header() | |
| test_migrate_formats_correct_header() | |
| if __name__ == "__main__": | |
| run_all_tests() |
| ADMIN_SECRET = os.getenv("ADMIN_SECRET") | ||
| if not ADMIN_SECRET: | ||
| if APP_ENV in ("production", "prod"): | ||
| raise ValueError("ADMIN_SECRET is required in production") | ||
| ADMIN_SECRET = "super-secret-admin-key" | ||
|
|
There was a problem hiding this comment.
ADMIN_SECRET falls back to a hardcoded default value when APP_ENV is not production. If a deployment forgets to set APP_ENV=production (or uses another value like staging), the admin protection becomes a known, guessable secret. Prefer failing fast when ADMIN_SECRET is unset (in all environments), or at least generate a random per-run dev secret and log it explicitly to avoid a stable default.
| @router.get("/migrate-formats") | ||
| def migrate_formats(db: Session = Depends(get_db)): | ||
| def migrate_formats( | ||
| x_admin_secret: str = Header(..., alias="X-Admin-Secret"), | ||
| db: Session = Depends(get_db), | ||
| ): |
There was a problem hiding this comment.
This endpoint performs a data migration (writes + commit) but is exposed as a GET. GET requests are commonly cached/retried/prefetched and are expected to be side-effect free, which increases the risk of accidental or repeated migrations. Consider switching this route to POST (or another non-GET verb) to better match HTTP semantics and reduce operational risk.
Co-authored-by: Horus0305 <98160215+Horus0305@users.noreply.github.com>
🚨 Severity: CRITICAL
💡 Vulnerability: The
/auth/migrate-formatsendpoint was publicly accessible without authentication, allowing any user to trigger database migrations and data modification.🎯 Impact: Unauthorized users could manipulate data or cause denial of service by repeatedly triggering migrations.
🔧 Fix:
ADMIN_SECRETvalidation toCricketGame/backend/core/config.py./auth/migrate-formatsinCricketGame/backend/api/auth.pyto requireX-Admin-Secretheader.secrets.compare_digestcheck.✅ Verification:
CricketGame/backend/test_auth.pywith tests covering missing, invalid, and valid secret scenarios.pytest.PR created automatically by Jules for task 2924955117721349805 started by @Horus0305