cog_gov_search() utility mode interpolates name into regexp_matches() unescaped #16
Closed
opened 2026-07-29 00:05:29 -04:00 by jared
·
3 comments
No Branch/Tag Specified
main
ci/mirror-canonical-tags
chore/release-47-badges-mirror-pr
docs/readme-perf-remeasure-56
feat/pagination-search-balances-57
feat/duckdb-threads-60
feat/cohort-predicates-58
fix/windows-backslash-paths
ci/mirror-to-github
ci/github-actions-matrix
feat/public-release-0.3.0
chore/fixture-sb203
ci/apt-https
fix/pushdown-pagination
feat/all-categories-37
fix/partial-coverage-signposting-9
fix/schema-v7
fix/cog-categories-balance-subtype
feat/cog-balances-25
feat/revenue-concepts-12
feat/expenditure-concepts-11
feat/coverage-disclosure-13
feat/complete-argument-18
fix/kodor-batch-14-15-16
fix/all-scoped-series-breaks-19
fix/regen-fixture-corpus-18
test/walkthrough-findings
feat/expenditure-concept
fix/3-url-trailing-slash
feat/phase-r3-signposting
fix/fixture-option-b-aggregates
feat/phase-r2-harmonization
feat/phase-r1-forward
feat/cog-gov-search-basket-mode
v0.4.0
Labels
Clear labels
kodor
kodor/feature-proposal
kodor/fix
kodor/needs-review
kodor/triaged
madison-walkthrough
severity/high
severity/low
severity/medium
south-guide
verdict/defect
verdict/definitional
kodor
kodor/feature-proposal
kodor/fix
kodor/needs-review
kodor/triaged
Kodor should process this issue
Kodor has written a feature proposal
Kodor should implement a fix (assigned to Kodor)
Kodor's work or failure needs Jared's review
Kodor has already triaged this issue (skip)
Surfaced while building the client-facing Southern API guide
needs
human
Cannot move without a person -- a decision, a check an agent cannot make, something outside the repo
origin
client
Came from a client ask
origin
obligation
Created by a change elsewhere
origin
review
Came from human review
origin
roborev
Promoted from a roborev finding
type
chore
Maintenance with no behaviour change
type
debt
Owed work -- docs, tests, cleanup a change obligated
type
decision
Needs a decision before work can proceed
type
defect
Something is wrong
type
feature
New capability
ws
api
Query verbs and results
ws
corpus
Corpus, mirror, provenance
ws
docs
Vignettes and guides
Assign a task to kodor
Kodor thinks this needs a feature.
Kodor should fix this
Kodor thinks the user is ready to review this.
Kodor is done with this issue.
Milestone
No items
No Milestone
Projects
Clear projects
No projects
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: Civilytics/uscogdata#16
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Filed from the Madison walkthrough audit (
cog_explorer/docs/walkthroughs/FINDINGS.md, 2026-07-28). Verdict: defect.Root cause
cog_gov_search()'s utility mode interpolates the caller'snamestraight into a DuckDB regex match, unescaped (R/search.R:102):The basket mode in the same file already does the right thing (
R/search.R:307):with the comment "Backslash-escape POSIX regex metacharacters so
nameis treated as a literal substring." The fix already exists in this file; utility mode just doesn't call it.Two distinct failure modes result, and they must not be conflated:
Findings resolved
qis interpolated unescaped into a regex match, not a substring match; a real government cannot be found by its own exact name, and ordinary typing can 500 the endpointReproduction (verbatim from FINDINGS.md, verified against the deployed API)
Six probes against
state=WI&type=citypin the semantics as regex, not substring:q=.= 608;q=M.dison= 1;q=^MADISON= 1;q=MADISON$= 0 (the stored name isMADISON CITY);q=Mad(i|o)son= 1.Reproduces directly on the bundled fixture corpus through the R verb, which is what the test asserts against:
Why it matters
cog_gov_search()is step one of essentially every workflow in this package and in the API built on it. The Fredonia case is the strongest evidence in the finding, and needs no regex literacy to read: a database returning zero results for a government's own on-file name, typed verbatim, is broken. The robustness half is reachable by nothing more adversarial than typing a real government's name one character at a time — a type-ahead client querying per keystroke would 500 on every parenthesized name, then find nothing once the name is complete.Unvalidated input reaching a regex engine is also a plausible catastrophic-backtracking (ReDoS) vector. That was deliberately not tested against the live production API to avoid degrading service — recorded as an unverified concern for maintainers to assess, not a confirmed finding.
Definition of done
nameinto a regex engine unescaped. Either route it through the existing.escape_regex()(one-line change, matching basket mode) or switch to a literal substring match (parameterisedLIKE/ILIKEwith%-wrapping and proper escaping of%/_).?cog_gov_search's documentation is updated: its@detailscurrently promises "matches the regex case-insensitively," which will no longer be true, and the basket-mode step-3 description ("Substring fallback: case-insensitive regex") should be restated in terms of the literal semantics both modes then share.tests/testthat/test-gov-search-literal-match.R→test_that("cog_gov_search() matches name literally, not as an unescaped regex", ...). Against the fixture it assertscog_gov_search("FREDONIA (BRISCOE) CITY")returns exactly one row withcanonical_govid == "052117184386"; thatcog_gov_search(".", state = "WI", type = "city")returns 0 rows (no Wisconsin city name contains a literal period — verified against the raw registry, not through the verb under test); and thatcog_gov_search("[")returns 0 rows without raising. Remove theskip()on line 1 of the test body to activate.(The test does not assert on
q=St. Louis. Under correct literal matching that search still returns 0 rows, because the stored name isST LOUIS CITYwith no period — the finding'sSt. Louisexample demonstrates today's over-matching semantics, not a row the fix will make findable. Noted in the test file.)Cross-reference
cog-apiowns two follow-ups: documentingq(name and matching semantics) on both self-description surfaces — finding F-002 — and mapping any residual malformed-input error to a 400 rather than a 500, as defence in depth once this fix removes the cause.Severity: high. Verdict: defect.
Cross-repo note: when this lands, two API documentation surfaces need a one-sentence deletion.
cog-api PR #18 documents
q=on/governmentsfor the first time (cog-api#11). Sincecog_gov_search()utility mode still matchesgov_nameas an unescaped regex, both surfaces carry a caveat:Documenting the post-fix semantics before the fix would have made the docs lie about the surface a caller actually meets, so the caveat is deliberate and temporary.
When this issue closes, delete that sentence from both:
api/data-dictionary.md— theqrow in### Filtersapi/llms.txt— theSTART HEREblockThe rest of the wording (case-insensitive substring against
gov_name, not anchored, not exact, ordered by ACS population descending) already describes the post-fix behaviour and needs no change.Taking this over. No kodor activity since assignment on 2026-07-29 — no branch, no PR, no comment — so I am picking it up rather than leaving it queued. Reassign back if kodor is simply slow and you would rather it land there; I will not push until this repo is otherwise quiet.
Being fixed together with the other two
kodor/fixissues in one PR: they are all single-file, all have committed acceptance tests, and two of them touch the same documentation surfaces.Fixed in PR #22. One consequence the issue did not anticipate, flagged rather than buried:
Anchored exact-match searches stop working, and utility mode no longer offers any exact-match option.
Two existing tests used regex anchors as their exact-match idiom:
With
nameescaped, those now search for the literal characters^and$and return nothing. Both were updated to the bare names, which still resolve to exactly one row each once scoped bystate/type— verified against the fixture, not assumed.That is fine for those two call sites, but the general capability is gone: utility mode is now substring-only, so
cog_gov_search("BROWARD COUNTY")would also match a hypotheticalNORTH BROWARD COUNTY .... Basket mode is unaffected — it still does an exact case-insensitive equality pass first, then falls back to substring.If exact matching should stay reachable from utility mode, the clean shape is an explicit argument (
match = c("substring", "exact")) rather than a return to regex, which is what produced the 500s and the silent over-matching in the first place. Say the word and I will add it; leaving it out for now, since the issue asked for literal matching and nothing in the package depends on the anchors any more.