Skip to content

[Bug] POST /leaderboard/update is unauthenticated #309

Description

@arpit2006

name: Bug report
title: "[Bug] POST /leaderboard/update is unauthenticated — scores can be gamed by anyone"
labels: bug, security

What happened

POST /leaderboard/update accepts any github_username and any is_pr_merged: true payload without any authentication or verification. There is nothing preventing a user from sending arbitrary POST requests to inflate their own (or any other contributor's) leaderboard score.

The leaderboard is designed to reward real contributors who close issues and merge PRs, but right now the data is fully user-controlled and untrustworthy.

Steps to reproduce

  1. Start the backend.
  2. Run the following command:
    curl -X POST http://localhost:8000/leaderboard/update \
      -H "Content-Type: application/json" \
      -d '{"github_username": "any_username", "pr_description": "Closes #1 Closes #2 Closes #3", "fixes_passed": 100, "is_pr_merged": true}'
  3. Call GET /leaderboardany_username now has a fabricated score of 305 points.
  4. Repeat step 2 as many times as you want. There is no rate limit, no auth, and no validation against real GitHub PR data.

Expected behaviour

Only a trusted, verifiable caller can write to the leaderboard. The recommended options (pick one):

Option A — GitHub Actions webhook (recommended): The endpoint requires an X-Hub-Signature-256 HMAC header signed with a LEADERBOARD_WEBHOOK_SECRET environment variable, matching the standard GitHub webhook payload format. Only GitHub's servers (triggered by a merged PR event) can post valid signatures.

Option B — Existing API key guard: Apply the same HTTPBearer authentication dependency used by other sensitive endpoints (introduced in PR #295) so that only callers with the server's API key can update the leaderboard.

Actual behaviour

Relevant code — backend/app/main.py, lines 1068–1091:

@app.post("/leaderboard/update")
async def update_leaderboard_endpoint(req: LeaderboardUpdateRequest):
    # No authentication check whatsoever
    pattern = r"(?i)(?:close|closes|...)...\s+#(\d+)"
    matches = re.findall(pattern, req.pr_description)
    findings_closed = len(set(matches))
    ...
    await upsert_contributor_stat(...)
    return {"status": "success", ...}

Environment

Field Value
OS Any
Python version 3.10+
PatchPilot version / commit main

Logs

INFO:     127.0.0.1 - "POST /leaderboard/update HTTP/1.1" 200 OK

No warning, no auth failure — the score is silently written.

Additional context

Option B implementation sketch (simpler, pairs with existing security infra):

from app.utils.security import verify_api_key  # or however PR #295 exposed the dependency

@app.post("/leaderboard/update")
async def update_leaderboard_endpoint(
    req: LeaderboardUpdateRequest,
    _: str = Depends(verify_api_key),  # ← add this
):
    ...

Option A requires reading the raw request body to verify the HMAC before Pydantic parses it — slightly more involved but fully automated when triggered by a GitHub webhook.

Acceptance criteria:

  • An unauthenticated POST /leaderboard/update request returns 401 Unauthorized.
  • An authenticated request (valid API key or valid HMAC signature) succeeds as before.
  • A test in tests/test_leaderboard_security.py asserts 401 for missing/invalid credentials.
  • The chosen auth method is documented in backend/README.md.

Metadata

Metadata

Assignees

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions