Describe the bug
The goals sync endpoint (src/app/api/goals/sync/route.ts) reads a repo value from the database and interpolates it directly into a GitHub Search API query string without any validation or encoding:
// Line 83-85: repo comes from DB with no validation
const repo =
(goal as any).repo ||
(goal as any).repository ||
(goal as any).repo_name ||
null;
// Line 90: directly interpolated into the query
const repoQualifier = repo ? `+repo:${repo}` : "";
// Line 92-93: unsanitized value in URL
const ghRes = await fetch(
`${GITHUB_API}/search/commits?q=author:${session.githubLogin}${repoQualifier}+author-date:${weekStart}..${weekEnd}&per_page=100&page=${page}`,
...
);
Problems:
-
Query injection — If a malicious or malformed repo value is stored in the database (e.g., via a compromised admin panel, SQL injection elsewhere, or a migration bug), it can inject arbitrary qualifiers into the GitHub Search API query. For example, a repo value of owner/repo+author:victim would alter the search scope.
-
Type safety bypass — The code uses (goal as any) to access three different possible column names (repo, repository, repo_name), completely bypassing TypeScript's type system. The .select() call on line 50 only selects "id, unit, repo, repository, repo_name" but there's no guarantee these columns exist or contain valid repository identifiers.
-
No URL encoding — Even for legitimate repo names, special characters (spaces, +, #) would corrupt the query string since the value is not passed through encodeURIComponent().
-
Inconsistent with contributions route — The contributions route (src/app/api/metrics/contributions/route.ts) uses URL and searchParams.set() for safe query construction, while this route uses raw string interpolation.
Expected behavior:
- The
repo value should be validated against a pattern like ^[a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+$ before use
- The query should be constructed using
URL + searchParams (matching the pattern in the contributions route)
- The
(goal as any) casts should be replaced with a proper typed interface
Suggested fix:
function isValidRepoQualifier(repo: string): boolean {
return /^[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+$/.test(repo);
}
// ...
const repoQualifier = repo && isValidRepoQualifier(repo) ? ` repo:${repo}` : "";
const searchUrl = new URL(`${GITHUB_API}/search/commits`);
searchUrl.searchParams.set(
"q",
`author:${session.githubLogin}${repoQualifier} author-date:${weekStart}..${weekEnd}`
);
searchUrl.searchParams.set("per_page", "100");
searchUrl.searchParams.set("page", String(page));
File location: src/app/api/goals/sync/route.ts (lines 83–95)
Describe the bug
The goals sync endpoint (
src/app/api/goals/sync/route.ts) reads arepovalue from the database and interpolates it directly into a GitHub Search API query string without any validation or encoding:Problems:
Query injection — If a malicious or malformed
repovalue is stored in the database (e.g., via a compromised admin panel, SQL injection elsewhere, or a migration bug), it can inject arbitrary qualifiers into the GitHub Search API query. For example, a repo value ofowner/repo+author:victimwould alter the search scope.Type safety bypass — The code uses
(goal as any)to access three different possible column names (repo,repository,repo_name), completely bypassing TypeScript's type system. The.select()call on line 50 only selects"id, unit, repo, repository, repo_name"but there's no guarantee these columns exist or contain valid repository identifiers.No URL encoding — Even for legitimate repo names, special characters (spaces,
+,#) would corrupt the query string since the value is not passed throughencodeURIComponent().Inconsistent with contributions route — The contributions route (
src/app/api/metrics/contributions/route.ts) usesURLandsearchParams.set()for safe query construction, while this route uses raw string interpolation.Expected behavior:
repovalue should be validated against a pattern like^[a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+$before useURL+searchParams(matching the pattern in the contributions route)(goal as any)casts should be replaced with a proper typed interfaceSuggested fix:
File location:
src/app/api/goals/sync/route.ts(lines 83–95)