Fix GitHub issue search repository scope - #49
Conversation
fca1c81 to
3984b64
Compare
|
Temporarily closing to re-trigger stuck CI checks. |
bb097d6 to
73480c5
Compare
There was a problem hiding this comment.
Review: Fix GitHub issue search repository scope
I reviewed the diff, ran the new tests (9 passing), tsc --noEmit (clean), and oxlint (only a pre-existing warning). The fix is sound and well-targeted. Notes below.
What the fix does well
- Two independent layers.
buildIssueSearchQueryquotes every caller-controlled fragment (text,author,assignee,labels) viaJSON.stringify, so an injectedrepo:/ORqualifier is matched as a literal phrase rather than parsed as syntax.assertIssueSearchResultsInRepothen re-derives the repo boundary from each result'shtml_urland rejects anything outsidehttps://github.com/<owner>/<repo>/issues/.... Defense-in-depth: even if quoting were bypassed, the result check still contains the blast radius. - Validation runs on cache hits too.
assertSearchScope(results)is called after#loadCachedWithEtagreturns (github.ts:2592), not only on the fresh path (github.ts:2582), so a poisoned or stale cache entry can't leak cross-repo results. Bumping the cache key tosearch-issues-scoped-v1correctly orphans old entries that lack the stored source URL, andstorage-schema.mdwas updated to match. - Only validated data is cached.
CachedIssueSearchResultstores justhtml_url+ theGitHubIssueSummary(nobodyMarkdown— that lives only onGitHubIssueDetails), matching the "avoid caching issue bodies" goal. state/sort/directionneed no quoting — they're constrained by string-literal unions at the RPC type boundary, so leavingstate:${query.state}unquoted is fine.- Correct scope of the fix. The PR search path (
#searchPullSummaries) goes through the REST/repos/{owner}/{repo}/pullsendpoint with localpullResponseMatchesSearchfiltering, so it never had therepo:-escape exposure that/search/issuesdid. Fixing only the issue path is right. - The
advanced_search: trueaddition is serialized as"true"(viaString(value)), which the newsearchIssuesConditionaltest asserts.
One behavioral change worth confirming (not blocking)
Previously PRs returned by the search were silently dropped (.filter(item => !item.pull_request)); now a /pull/ URL makes assertIssueSearchResultsInRepo throw ("non-issue result"), failing the whole page rather than skipping the row. With is:issue + advanced_search GitHub shouldn't return PRs, so in practice this should never fire — but it converts a previously-tolerated upstream quirk into a hard error for the caller. If you'd rather keep the search resilient, consider filtering non-issue results out and only throwing on the true cross-repo case (a wrong owner/repo), which is the actual security boundary. The scope-mismatch logger.warn is a nice touch either way.
Nits
assertIssueSearchResultsInReporeadsresult.html_urlpositionally from the path (resultOwner,resultRepo,resultKind); the tests cover prefix-only names, wrong host, malformed URLs, and the pull/issue distinction, which is good coverage for the parsing.
Overall this is a clean, minimal fix that closes the scope-escape and adds solid regression tests. LGTM aside from confirming the PR-result-throws behavior is intentional.
|
Review posted to PR #49. SummaryI reviewed PR #49 ("Fix GitHub issue search repository scope"), verified it locally (9 tests pass, The fix is sound. The vulnerability was that GitHub issue search built a
Things I verified as correct:
One non-blocking flag I raised: the old code silently filtered out PRs from results; the new code throws on a |
GitHub issue search was relying on an appended
repo:qualifier, but caller-controlled search syntax could escape that scope.This keeps the repository boundary local by quoting search inputs, validating results before caching or returning them, and retaining the source URL for cache revalidation. It also logs blocked scope mismatches and avoids caching issue bodies.
Added focused regression tests for injection attempts, case handling, and malformed result URLs.
Tested with the full test suite, type checks, and lint.