fix(query): reject reserved encoded-query characters in condition values - #653
fix(query): reject reserved encoded-query characters in condition values#653michaeldcanady wants to merge 7 commits into
Conversation
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #653 +/- ##
==========================================
+ Coverage 86.57% 86.70% +0.13%
==========================================
Files 337 338 +1
Lines 8439 8538 +99
==========================================
+ Hits 7306 7403 +97
- Misses 1132 1134 +2
Partials 1 1
🚀 New features to boost your workflow:
|
Condition values were written into the encoded query verbatim, so a value containing ^, ,, or @ could break out of its term and append arbitrary clauses to sysparm_query (encoded-query injection). ServiceNow encoded queries provide no escape sequence for these structural characters, so values are now validated at construction time: a rejected value yields an error Condition that surfaces through the existing Error() channel and propagates via And()/Or(). Internally composed date literals (OnSpecialty, JS, NewDateTimeValue) keep their intentional @ / javascript: separators and bypass validation. Fixes #645
f96392b to
2b7bacc
Compare
|
/oc review |
Overall (Request changes)The core approach is sound: validating the However, the trust-boundary carve-out has a hole that matters for a 1.
|
…ns safely Addresses review findings on the #645 injection fix: - JS(), Javascript(), and OnSpecialty() took caller-supplied text that was embedded verbatim into the encoded query via the trusted DateTimeValue fast path, leaving the exact clause-injection this fix set out to close reachable with Error() == nil. Fragments are now validated at their public entry points (rejecting ^ , @) and surface as error conditions. - String() panicked on a rejected condition (nil AST node), including combinations embedding one — turning the recommended missed-Error() failure mode from a bad query into a production panic. Error conditions now carry an <invalid query> sentinel node, every ast node's Accept is nil-receiver safe, and String() renders the fail-closed placeholder for nodeless conditions instead of an empty (unfiltered) sysparm_query. - Cover dateTimeBinary's string and default branches through both happy and rejected paths.
|
/oc review |
Overall (Approve)Both blockers from the previous review are properly resolved in 1. Fragment validation rejects commas inside legitimate JavaScript expressions
2. Consider per-operator reserved-character sets for plain valuesRelated and longer-term: 3. Unused
|
Visitor traversal previously discarded structural faults: Accept returned nothing, so a nil child or unknown operator rendered a silently corrupted encoded query. - internal/ast: Accept and every Visit method now return an error; traversal short-circuits on the first fault and reports the ErrNilNode / ErrNilChild / ErrUnknownOperator sentinels (new errors.go). The split Literal/Unary visitor interfaces are consolidated into a single Visitor, and stale per-operator "implemented" comments are dropped. - query: Condition.Query() (string, error) joins construction-time faults (Error) with render-time ones; String() keeps its fail-closed placeholder behavior on structural faults while still rendering rejected sides as <invalid query> terms. Doc comments now use godoc links.
Documentation CI/CD failed on six Microsoft/Vale errors introduced with the label-conventions page and batch tips: - "Auto-applied" / "auto-synced": Microsoft.Auto rejects hyphenation. - "e.g.,": Microsoft.Foreign requires "for example". - "is **not**": Microsoft.Contractions requires "isn't". - "triaged" / "Triaged": unknown to Vale.Spelling; accepted via a new (?i)triag(ed|ing) entry alongside the existing (?i)triaging.
|
/oc review |
Overall (Approve)Both blockers from the first review remain fixed, and the two commits added since my last review ( Remaining findings below are non-blocking refinements. One process note: 1. AST sentinel docs promise context-wrapping that isn't implementedThe comment says visitors "wrap these with node context (position, element index) so the resulting message identifies where traversal failed," but 2.
|
Responds to review findings 2 and 3 on #653: - And()/Or() with no conditions return an error Condition matching ErrNoConditions instead of nil, whose String()/Error() previously panicked on the nil interface. - Combining against a nil Condition — as a method argument, a variadic element, or a typed-nil pointer — returns an error Condition matching ErrNilCondition instead of panicking at other.ToNode(). Guards use conversion.IsNil so wrapped nil pointers are caught too. - Withdraw Condition.Query() before merge: adding a method to the exported interface would break external implementers within v2 semver. Render faults stay fail-closed through String()'s placeholder degradation; construction faults surface via Error(). New sentinels live in query/errors.go; TestCombinatorNilSafety covers all ten degenerate paths, including the typed-nil trap.
^ remains rejected in every value and fragment — it is the only character that can break out of a term and append clauses (#645). , is now reserved only in IN / NOT IN values and @ only in BETWEEN values, where the builder itself emits them as separators; elsewhere they are ordinary literals, so "Smith, John" and "user@example.com" pass through unchanged. Date-time composite fragments (JS(), OnSpecialty()) reject ^ and @ but allow commas, which multi-argument gs.* calls need. Docs and package comment updated to match.
|
@Atishyy27 would you mind taking a look and see if this addresses your concerns? |
Resolve prose conflicts by taking main's equivalent rewordings (Applied automatically / synced automatically, unbolded isn't), which supersede the PR's Vale-driven edits.
Closes #645
What
Condition values in the
querybuilder were written into the encoded query verbatim, so a value containing^,,, or@could break out of its term and append arbitrary clauses tosysparm_query— the encoded-query injection described in #645:How
query.validateQueryValuerenders any value via%vand rejects it if it contains a metacharacter that would be structural where that value lands:^(clause separator) in every value, plus,for IN / NOT IN values and@for BETWEEN values. Those two are ordinary literals elsewhere ("Smith, John","user@example.com"), since only the builder's own rendering makes them separators. Errors name the field, operator, value, and offending character.ConditionwhoseError()is non-nil, propagating viaerrors.Jointhrough.And()/.Or()(same pattern asNumberField.Between/DateTimeField.Between).query, notinternal/ast: internally composed date literals (OnSpecialty,JS,NewDateTimeValue) intentionally contain@/javascript:separators and bypass value validation. Their caller-supplied fragments are validated against^and@only — commas stay legal there, so multi-argumentgs.*calls likegs.dateGenerate('2024-01-01','00:00:00')keep working; an invalid fragment surfaces here as an error condition.Today(),Yesterday(), etc. are unchanged.BaseField.multibecame a package-level generic (multi[T ast.Primitive]) since Go methods can't declare type parameters; behavior ofIsOneOf/IsNotOneOfis unchanged.:::warningon the query-builder page explaining the risk, which characters are rejected where, and advising anError()check before serializing, plus an updated package comment.Scope
Deliberately limited to the query builder. Field names (
Where("x^active=false")) remain unvalidated — constructors return field structs with no error channel, so closing that needs a small refactor — and rawsysparm_querystrings passed directly to request builders behave exactly as before.Edge-case behavior changes
Degenerate combinator inputs now fail closed instead of returning nil or panicking. Callers checking
== nilon combinator results will see different (safer) behavior; callers usingError()/String()are unaffected:And()/Or()with no arguments previously returned a nilCondition; they now return an error Condition whoseError()matches the newquery.ErrNoConditions..And()/.Or()or the variadic combinators previously panicked when combined; it now returns an error Condition matching the newquery.ErrNilCondition.NewErrorConditioncarries a placeholder node, soString()on a rejected or malformed condition renders<invalid query>instead of panicking.Testing
query/validation_test.go(~60 cases): the issue's exact repro + guard-clause composition, every value-taking method across all four field types, each reserved character in each operator context (rejected exactly where structural), false-positive resistance (=,!,%, unicode, exponent floats render unchanged; builder-emitted,/@still work), trusted-composite carve-outs, and curveballs (afmt.StringerwhoseString()smuggles a^).go test -tags integration ./tests/integration/v2/),golangci-lint run ./...clean, gofmt/vet clean.