Skip to content

fix: parameterize SQON-to-SQL filter values, closing a SQL injection - #219

Open
justincorrigible wants to merge 6 commits into
mainfrom
fix/sqon-sql-injection
Open

fix: parameterize SQON-to-SQL filter values, closing a SQL injection#219
justincorrigible wants to merge 6 commits into
mainfrom
fix/sqon-sql-injection

Conversation

@justincorrigible

@justincorrigible justincorrigible commented Aug 20, 2026

Copy link
Copy Markdown
Member

Summary

processFilterOperator spliced fieldName/value directly into sql.raw() text via an unescaping formatForSQL helper.

Any SQON filter reaches this path (e.g. the organization-scoped query endpoint), so a crafted fieldName or value containing a quote could alter the WHERE clause's boolean structure, not just the intended predicate, since drizzle's outer and() wraps the whole clause group in one pair of parens without isolating each individual child condition.

Bind fieldName and value through drizzle's sql template instead, which parameterizes any interpolated value rather than inlining it as text. Regression coverage added for a fieldName containing SQL metacharacters.

Changes cover #43

Note: fieldName still has no allowlist against the dictionary's real field names, but it's harmless now that it's parameterized. worth addressing on a separate PR.

Readiness Checklist

  • Self Review
    • I have performed a self review of code
    • I have run the application locally and manually tested the feature
    • I have checked all updates to correct typos and misspellings
  • Formatting
    • Code follows the project style guide
    • Autmated code formatters (ie. Prettier) have been run
  • Local Testing
    • Successfully built all packages locally
    • Successfully ran all test suites, all unit and integration tests pass
  • Updated Tests
    • Unit and integration tests have been added that describe the bug that was fixed or the features that were added
  • Documentation
    • All new environment variables added to .env.schema file and documented in the README
    • All changes to server HTTP endpoints have open-api documentation
    • All new functions exported from their module have TSDoc comment documentation

processFilterOperator spliced fieldName/value directly into sql.raw()
text via an unescaping formatForSQL helper. Any SQON filter reaches
this path (e.g. the organization-scoped query endpoint), so a crafted
fieldName or value containing a quote could alter the WHERE clause's
boolean structure, not just the intended predicate, since drizzle's
outer and() wraps the whole clause group in one pair of parens without
isolating each individual child condition.

Bind fieldName and value through drizzle's sql template instead, which
parameterizes any interpolated value rather than inlining it as text.
Regression coverage added for a fieldName containing SQL metacharacters.

Reported by the iMicroSeq Submission Service session.
…Filtered

Same vulnerability class as fix/sqon-sql-injection's convertSqonToQuery.ts
fix, found while auditing the codebase for other sql.raw() call sites with
non-constant input: getSubmittedDataFiltered spliced dataField/dataValue
directly into sql.raw() text with no escaping.

This one is more severe than the SQON case: dataValue is not a query
parameter a caller supplies at read time, it's a value read back out of a
submitter's own previously-submitted record (searchDataRelations.ts,
viewMode.ts, submissionProcessor.ts all resolve foreign-key relationships
this way). A malicious value submitted once poisons every later query that
walks that record's dictionary relationships, a stored/second-order
injection rather than one confined to a single request.

Extracted the filter-building into buildDataFieldFilter, which binds both
values through drizzle's sql template instead of sql.raw(), and unit
tested it directly (parity case, an injection-shaped dataField, and a
statement-terminator/comment payload in dataValue) since the repository
factory itself has no existing unit-test coverage to extend.
…e TODO

The original regression test only covered a quote-injection fieldName on
the "in" operator. Add: the same fieldName injection through the gt
operator (proves the shared jsonbField construction is covered for scalar
filters too, not just arrays), an injection-shaped element inside an "in"
value array, and a combined statement-terminator/comment payload, the
canonical "worst case" shape a reviewer would look for by name.

Also drop parseSQON's "TODO: SQL sanitization" comment referencing
lyric#43: that's what this fix (and the sibling fix in
getSubmittedDataFiltered) addresses, so the TODO is now stale.
// An error will be thrown if the provided input is invalid.
return SQONBuilder.default.from(input);

// TODO: SQL sanitization (https://github.com/overture-stack/lyric/issues/43)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

😉

Issue #43 covers three items and only one is fixed here; the other two
(TSV sanitization, a broader endpoint-input audit) are separate, unstarted
work. Also records the fieldName-allowlist gap noted in the PR
description, so it isn't only living in a review comment that can get
lost.
Both fixes built the IN clause with a hand-rolled sql`${field} IN ${values}`
template instead of drizzle's own inArray() helper, which activeSubmissionRepository.ts
and submittedRepository.ts already use elsewhere for the same operation. Switched to
match, which also rejects an empty array up front instead of emitting invalid `IN ()`.

Every existing "in" test used a single-element array, which cannot distinguish
comma-expansion (one bound parameter per element, valid syntax) from binding the
whole array as one parameter (invalid IN syntax in Postgres). Verified directly
against drizzle's actual query output that comma-expansion is what happens, and
added a three-element regression test to lock it in. Flagged by review.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant