Skip to content

fix: add per-IP rate limiting and strict validation to badge endpoints#471

Open
advikdivekar wants to merge 4 commits into
Priyanshu-byte-coder:mainfrom
advikdivekar:fix/issue-453-badge-rate-limit-validation
Open

fix: add per-IP rate limiting and strict validation to badge endpoints#471
advikdivekar wants to merge 4 commits into
Priyanshu-byte-coder:mainfrom
advikdivekar:fix/issue-453-badge-rate-limit-validation

Conversation

@advikdivekar
Copy link
Copy Markdown
Contributor

Summary

Closes #453

The badge endpoints (/api/badge/commits and /api/badge/streak-shield) accepted any GitHub username with only a length check — no
authentication, no rate limiting. A single script looping thousands of usernames could exhaust the shared GITHUB_TOKEN quota (5,000 req/hr) and
take down the public profile page, leaderboard, and all badge endpoints simultaneously. Access patterns were also leaked unconditionally via

Changes

  • src/lib/badge-rate-limit.ts (new) — sliding-window, in-memory rate limiter scoped to badge endpoints (20 req/min per IP), mirrors the
    existing middleware pattern exactly. Falls back gracefully: unknown IP counts as "unknown", still rate limited.

  • src/app/api/badge/commits/route.ts — replaces the loose length check with GITHUB_USERNAME_RE (GitHub-spec: 1–39 chars, alphanumeric +
    hyphens, no leading/trailing hyphen). Adds per-IP rate limit gate before any processing. Returns 429 with Retry-After, X-RateLimit-* headers
    on excess. Removes all console.log statements. Upgrades Cache-Control to s-maxage=3600, stale-while-revalidate=86400.

  • src/app/api/badge/streak-shield/route.ts — same validation, same rate limit gate, same header cleanup.

Test plan

  • GET /api/badge/commits?user=torvalds → 200 SVG badge
  • GET /api/badge/commits?user=-invalid- → 400 {"error":"Invalid username"}
  • GET /api/badge/commits (no param) → 400 {"error":"Invalid username"}
  • GET /api/badge/streak-shield?user=torvalds → 200 SVG badge
  • GET /api/badge/streak-shield?user=-invalid- → 400
  • 21 rapid requests from the same IP → request 21 returns 429 with Retry-After header
  • Server logs contain no console.log of usernames during any of the above

Verification summary: All 7 criteria passed live against the running dev server — valid usernames return SVG (200), invalid usernames return 400,
missing param returns 400, and the 20 req/min limit correctly returns 429 with Retry-After/X-RateLimit-* headers on excess. No console.log of
usernames appeared in server output (only console.error for actual GitHub API failures, which is correct).

- Wrap req.json() in try/catch — returns 400 on malformed JSON
- Validate body is a non-null object before destructuring
- title: must be non-empty string, trimmed, max 100 characters
- target: must be integer in [1, 10000] — blocks 0, negatives, floats, NaN, overflow
- unit: clamped to 30 chars silently
- recurrence: unknown values default to 'none' explicitly
- All bounds defined as named constants

Fixes division-by-zero (target:0 → Infinity% progress bar) and
negative target making 0-progress goals show as 100% complete.

Closes Priyanshu-byte-coder#454
Badge endpoints accepted arbitrary GitHub usernames with no auth, no
rate limiting, and only a length check — a single script could exhaust
the shared GITHUB_TOKEN quota and DoS the whole platform. Adds a 20
req/min sliding-window rate limit per IP (shared with the existing
middleware pattern), replaces the length check with a GitHub-spec
username regex, and removes console.log calls that leaked user access
patterns to server logs. Fixes Priyanshu-byte-coder#453
@vercel
Copy link
Copy Markdown

vercel Bot commented May 20, 2026

@advikdivekar is attempting to deploy a commit to the PRIYANSHU DOSHI's projects Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions github-actions Bot added gssoc26 GSSoC 2026 contribution type:feature GSSoC type bonus: new feature labels May 20, 2026
@github-actions
Copy link
Copy Markdown

GSSoC Label Checklist 🏷️

@Priyanshu-byte-coder — please apply the appropriate labels before merging:

Difficulty (pick one):

  • level:beginner — 20 pts
  • level:intermediate — 35 pts
  • level:advanced — 55 pts
  • level:critical — 80 pts

Quality (optional):

  • quality:clean — ×1.2 multiplier
  • quality:exceptional — ×1.5 multiplier

Validation (required to score):

  • gssoc:approved — counts for points
  • gssoc:invalid / gssoc:spam / gssoc:ai-slop — does not score

Type labels (type:*) are auto-detected from files and title. Review and adjust if needed.
Points formula: (difficulty × quality_multiplier) + type_bonus

Copy link
Copy Markdown
Owner

@Priyanshu-byte-coder Priyanshu-byte-coder left a comment

Choose a reason for hiding this comment

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

Merge conflict — rebase on main to resolve. The implementation is solid (rate limiting, username regex, badge validation, goals route hardening, .gitignore cleanup) — once conflict is cleared this is ready to merge.

@Priyanshu-byte-coder Priyanshu-byte-coder added level:intermediate GSSoC: Intermediate difficulty (35 pts) type:security GSSoC type bonus: security (+20 pts) type:bug GSSoC type bonus: bug fix labels May 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gssoc26 GSSoC 2026 contribution level:intermediate GSSoC: Intermediate difficulty (35 pts) type:bug GSSoC type bonus: bug fix type:feature GSSoC type bonus: new feature type:security GSSoC type bonus: security (+20 pts)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CRITICAL : Badge Endpoints Accept Arbitrary GitHub Usernames Without Auth or Rate Limit

2 participants