Repository navigation
[WP4] LIKE-family wildcard injection + unbounded IN expansion #176
Copy link
Copy link
Closed as not planned
Labels
status: pending-design-work動工前需要更多的設計或研究動工前需要更多的設計或研究type: bugSomething isn't workingSomething isn't working
Description
此內容由 AI 產生(specification-mapper 全庫審查 2026-07-16)。
Work package WP4 · findings: SEC-01, MAINT-03 · worst severity: medium
From the 2026-07-16 whole-codebase review (branch jakarta, modules mapper/ + starter/). Each finding below is CONFIRMED by independent adversarial verification. File paths and line numbers reflect the review snapshot and may drift — treat them as starting points, not exact coordinates.
SEC-01 — LIKE-family specs concatenate user values into patterns without escaping % and _ wildcards
- Category / severity: security / medium · effort S · confidence high
- Location:
mapper/src/main/java/tw/com/softleader/data/jpa/spec/domain/Like.java:53 - Evidence: Like's constructor builds the pattern as "%" + value + "%" (Like.java:53) and toPredicate calls builder.like(getPath(root), Objects.toString(value)) with no escape character (Like.java:59). The same pattern appears in NotLike.java:54, StartingWith.java:53 (value + "%"), and EndingWith.java:53 ("%" + value). grep for escape/ESCAPE across mapper/src/main returns nothing — no escaping exists anywhere, and no cb.like overload with an escape char is used. The value is the user-submitted query-form field, which SimpleSpecificationResolver passes through verbatim.
- Failure scenario: A user submits "%" (or "a%b%c") into a field mapped with @SPEC(StartingWith.class) intended as a strict prefix filter: the pattern becomes "%%" and matches every row, turning a scoped search into a match-all (data exposure when the prefix was acting as a scoping restriction, e.g. tenant/department code). Pathological patterns like "%a%b%c%d%" force expensive full-scan LIKE evaluation on large tables — a cheap DoS vector on any search endpoint.
- Suggested fix: Escape \, % and _ in the user value before concatenation (e.g. value.replace("\", "\\").replace("%", "\%").replace("", "\")) and use the builder.like(expr, pattern, escapeChar) overload with '\' in Like, NotLike, StartingWith, and EndingWith. If literal-wildcard search is meant to stay supported, document it and offer escaped variants.
- Verification: [CONFIRMED] Traced the code directly. Like.java:52 builds the pattern via
super(context, path, "%" + value + "%")and Like.java:57-58 callsbuilder.like(getPath(root), Objects.toString(value))with no escape-char overload. NotLike.java (pattern"%"+value+"%",builder.notLike(...)), StartingWith.java (value + "%"), and EndingWith.java ("%" + value) all follow the identical no-escape pattern. grep for escape/ESCAPE across mapper/src/main/java returns nothing — no escaping helper and nocb.like(expr, pattern, escapeChar)usage anywhere. SimpleSpecificationResolver.buildSpecification passes the databind field value straight through (.value(value), line 83) into the spec construct…
MAINT-03 — In/NotIn expand unbounded user-supplied collections into a single IN (...) list
- Category / severity: maintainability / low · effort S · confidence high
- Location:
mapper/src/main/java/tw/com/softleader/data/jpa/spec/domain/In.java:64 - Evidence: In.toPredicate materializes the entire user-supplied Iterable into an Object[] and passes it to
getPath(root).in(...)(line 64) with no size limit or chunking; NotIn inherits the same expansion. The value comes straight from a query-POJO field bound from the request. - Failure scenario: A client posts a criteria object whose @SPEC(In.class) collection field carries tens of thousands of ids: the query blows past DB/driver parameter or in-list limits (SQL Server 2100 params, Oracle 1000 in-list elements) and fails with a SQLException, or degrades the DB with megabyte-sized SQL — a request-driven DoS vector.
- Suggested fix: Document the limit and validate collection size at construction (throwing a clear IllegalArgumentException), or chunk large collections into OR-ed IN groups.
- Verification: [CONFIRMED] Traced In.java lines 53-65. toPredicate (line 63-64) streams the whole user-supplied Iterable into Object[] and passes it to getPath(root).in(...) with no size limit or chunking; the constructor only validates instanceof Iterable, not size. NotIn mirrors this. The claim about code behavior is factually accurate, and the failure mechanism (exceeding SQL Server 2100-param / Oracle 1000 in-list limits, or oversized SQL) is real for any large collection. However, this is standard behavior for essentially every JPA/Spring Data IN wrapper (Hibernate pads but does not chunk IN lists); chunking/limiting is not a conventional responsibility of this thin specification layer, and the DoS re…
Full report: see the codebase-review-2026-07-16 branch (linked on the parent tracking issue).
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
status: pending-design-work動工前需要更多的設計或研究動工前需要更多的設計或研究type: bugSomething isn't workingSomething isn't working
Activity
shihyuho commentedon Jul 16, 2026
Triage Notes - needs human
Category: bug (WP4 bundles a security defect, SEC-01, and a robustness finding, MAINT-03)
Summary: LIKE-family specifications concatenate user values into patterns without escaping
%/_wildcards, andIn/NotInexpand unbounded user collections into a singleIN (...)list — both confirmed present, but both fixes hinge on a maintainer policy decision.SEC-01 — LIKE-family wildcard injection
Current behavior:
Like,NotLike,StartingWith, andEndingWith(all extendingSimpleSpecification<T>) build their LIKE pattern by raw string concatenation in the constructor —"%" + value + "%",value + "%","%" + value— andtoPredicatecallsCriteriaBuilder.like/notLike(Expression, String)with no escape-character overload. Any%or_in the user-supplied value is interpreted as a SQL wildcard. A value of"%"into aStartingWithprefix filter yields pattern"%%", matching every row; pathological patterns like"%a%b%c%d%"force expensive scans. No escaping helper exists anywhere in the module.Desired behavior (pending design decision): User-supplied
%,_, and the escape character itself should be treated as literals in LIKE matching, using aCriteriaBuilder.like(expr, pattern, escapeChar)overload — UNLESS literal-wildcard passthrough is a deliberate, supported feature, in which case the behavior must be documented and escaped matching offered as an explicit opt-in / separate variant.MAINT-03 — unbounded IN expansion
Current behavior:
In.toPredicatestreams the entire user-suppliedIterableinto anObject[]and passes it toPath.in(...)with no size limit or chunking; the constructor only checksinstanceof Iterable.NotIninherits this. A collection of tens of thousands of elements can exceed DB/driver limits (e.g. SQL Server 2100 params, Oracle 1000 in-list elements) and fail, or emit oversized SQL.Desired behavior (pending design decision): Bound or chunk the expansion — but the review's own verification note flags that chunking/limiting is unconventional for a thin specification layer (Hibernate itself pads rather than chunks IN lists), so whether to act at all, and how, is a judgment call.
Key interfaces
tw.com.softleader.data.jpa.spec.domain.Like<T>,NotLike<T>,StartingWith<T>,EndingWith<T>— constructor(Context, String, Object),toPredicate(Root, CriteriaQuery, CriteriaBuilder)tw.com.softleader.data.jpa.spec.domain.In<T>,NotIn<T>— same signaturestw.com.softleader.data.jpa.spec.domain.SimpleSpecification<T>— base class holdingcontext,path,valuejakarta.persistence.criteria.CriteriaBuilder#like(Expression, String, char)— the escape-char overload not currently usedAcceptance criteria (to be finalized once the decisions below are made)
%/_/escape-char per the chosen policy, verifiable by a JPA/H2 integration test asserting a value containing%does not behave as a wildcard (or does, if passthrough is the chosen contract).Like/NotLike/StartingWith/EndingWithis documented (Javadoc + site docs) and consistent across all four types.In/NotInbehavior for oversized collections matches the chosen policy (limit/chunk/no-op), with a test covering the boundary.Out of scope
SimpleSpecificationResolvervalue binding.Needs human because:
%/_silently changes the observable results ofLike/StartingWith/EndingWithfor any existing caller that today relies on passing wildcards through. Deciding whether that break is acceptable, whether escaping should be default-on vs opt-in, and whether to keep literal-wildcard support via new escaped-variant specs are public-API-shape and breaking-change judgments an autonomous agent cannot make. Without that decision the acceptance criteria are not determinable (the "or does, if passthrough is the chosen contract" fork above).Either decision (per-finding) can be split off into an agent-ready follow-up once the contract is fixed.
shihyuho commentedon Jul 20, 2026
Superseded by #194 in the execution map #204, which carries the same findings (SEC-01, MAINT-03). This 2026-07-16 static plan is being retired in favour of that map.