fix: validate repo field before GitHub query construction in goals sync#1783
Merged
Priyanshu-byte-coder merged 1 commit intoMay 31, 2026
Conversation
Root cause
----------
The goals sync route read an optional repository filter from stored goal
rows and interpolated the raw value directly into a GitHub Search API
URL:
const repo =
(goal as any).repo ||
(goal as any).repository ||
(goal as any).repo_name ||
null;
const repoQualifier = repo ? `+repo:${repo}` : "";
fetch(`…/search/commits?q=author:${login}${repoQualifier}+author-date:…`)
A stored value such as "octocat/Hello-World+author:victim" would inject
an additional author qualifier into the GitHub Search API query, silently
expanding the commit search beyond the authenticated user's own history
and producing incorrect goal progress counts.
Additionally, `(goal as any)` bypassed TypeScript type safety across all
three field name variants, and the raw query string used `+` concatenation
instead of URLSearchParams, making the parameter boundary unclear.
Fix
---
src/app/api/goals/sync/route.ts
- Introduce REPO_IDENTIFIER_RE — a strict regex accepting only
"owner/repo" where owner is 1–39 chars (alphanumeric + hyphens,
no leading/trailing hyphens) and repo is 1–100 chars (alphanumeric,
dots, hyphens, underscores). The same constraint previously applied
to the public repo-analytics endpoint.
- Export extractValidRepoFromGoal(goal: ActivityGoal): string | null
which reads the first non-null value from goal.repo → goal.repository
→ goal.repo_name, trims it, validates it against the regex, and
additionally rejects "." and ".." as repo names. Any value that does
not match returns null so the repo filter is omitted from the query.
- Define the ActivityGoal typed interface to replace (goal as any),
documenting the three optional field names and providing proper type
coverage for downstream code.
- Move GitHub Search URL construction to URLSearchParams so the combined
qualifier string is encoded as a single atomic value and cannot be
split by embedded special characters. Applied to both the commit
search loop and the PR search call.
test/goals-sync-repo-validation.test.ts — 28 new tests:
Valid inputs: standard, dots/underscores, org hyphens, all three
field names, whitespace trimming, max lengths
Null/empty: all-null, empty string, whitespace-only
Query injection (regression for Priyanshu-byte-coder#1757): embedded +author:, space-
separated qualifier, ampersand, URL-encoded %2B
Extra path segments: three-segment, four-segment paths
Path traversal: owner/.., owner/.
Invalid formats: bare name, leading/trailing slash, hyphen rules,
owner >39 chars, repo >100 chars
Field priority: repo > repository > repo_name; invalid higher-
priority field does not fall through to a valid lower-priority field
Closes Priyanshu-byte-coder#1757
|
@Ridanshi 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. |
GSSoC Label Checklist 🏷️@Priyanshu-byte-coder — please apply the appropriate labels before merging: Difficulty (pick one):
Quality (optional):
Validation (required to score):
|
ac437b0
into
Priyanshu-byte-coder:main
4 of 5 checks passed
|
🎉 Merged! Thanks for contributing to DevTrack. If the project has been useful to you, a ⭐ star on the repo is the easiest way to support it — it helps DevTrack get discovered by more developers. Keep an eye on open issues for your next contribution! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1757
Problem
POST /api/goals/syncreads an optional repository filter from stored goal rows and interpolated the raw value directly into a GitHub Search API URL without any validation:A stored value containing GitHub Search qualifiers — for example
octocat/Hello-World+author:victim— would be interpolated verbatim into the query string:This expands the search scope beyond the authenticated user's commits, counting commits from
victimtoward the goal's progress total. The same pattern applies to any qualifier that the GitHub Search API recognises (language:,org:,is:, etc.).Root cause
Three compounding problems in
src/app/api/goals/sync/route.ts:repovalue before use.(goal as any)throughout — bypasses TypeScript and obscures that three differently-named fields all feed the same code path.Fix
Validation —
extractValidRepoFromGoal()Validates the stored value against a strict
owner/reporegex before any use:.and..as repo namesAny value that does not match is treated as absent — the repo qualifier is simply omitted from the query and sync proceeds without a repository filter. The function is exported so it can be unit-tested independently.
Safe query construction
Moved all GitHub Search URL construction to
URLSearchParams:URLSearchParamsencodes the entireqstring as a single value, so the parameter boundary is structurally enforced.Type safety
Replaced
(goal as any)with a typedActivityGoalinterface:Tests —
test/goals-sync-repo-validation.test.ts(28 new tests)+author:victim, space-separated qualifier,&, URL-encoded%2Bowner/repo/issues), four-segmentowner/..,owner/.repo>repository>repo_name; invalid first field does not fall through to valid secondAll 28 pass. The one pre-existing failure in
test/dateUtils.test.tsis a timezone boundary issue unrelated to this change.