Skip to content

[WP4] LIKE-family wildcard injection + unbounded IN expansion #176

Closed as not planned

Description

@shihyuho

此內容由 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 calls builder.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 no cb.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).

Activity

  1. added theissue type on Jul 16, 2026
  2. shihyuho commented on Jul 16, 2026

    @shihyuho
    MemberAuthor

    This was generated by AI during triage.

    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, and In/NotIn expand unbounded user collections into a single IN (...) list — both confirmed present, but both fixes hinge on a maintainer policy decision.

    SEC-01 — LIKE-family wildcard injection

    Current behavior: Like, NotLike, StartingWith, and EndingWith (all extending SimpleSpecification<T>) build their LIKE pattern by raw string concatenation in the constructor — "%" + value + "%", value + "%", "%" + value — and toPredicate calls CriteriaBuilder.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 a StartingWith prefix 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 a CriteriaBuilder.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.toPredicate streams the entire user-supplied Iterable into an Object[] and passes it to Path.in(...) with no size limit or chunking; the constructor only checks instanceof Iterable. NotIn inherits 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 signatures
    • tw.com.softleader.data.jpa.spec.domain.SimpleSpecification<T> — base class holding context, path, value
    • jakarta.persistence.criteria.CriteriaBuilder#like(Expression, String, char) — the escape-char overload not currently used

    Acceptance criteria (to be finalized once the decisions below are made)

    • LIKE-family specs treat user %/_/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).
    • The chosen contract for Like/NotLike/StartingWith/EndingWith is documented (Javadoc + site docs) and consistent across all four types.
    • In/NotIn behavior for oversized collections matches the chosen policy (limit/chunk/no-op), with a test covering the boundary.
    • No regression to existing spec resolution tests.

    Out of scope

    • Broader SQL-injection concerns beyond LIKE wildcard semantics (path/field binding is not user-controlled here).
    • Changing SimpleSpecificationResolver value binding.

    Needs human because:

    1. SEC-01 is a behavioral / breaking change to a published library. Escaping %/_ silently changes the observable results of Like/StartingWith/EndingWith for 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).
    2. MAINT-03 needs a policy value and a should-we-even decision. Any fix requires picking a concrete size limit (a magic number) and choosing chunk-into-OR-groups vs fail-fast — and the review itself notes this is unconventional for a thin spec layer, so the maintainer must first decide whether to act at all.

    Either decision (per-finding) can be split off into an agent-ready follow-up once the contract is fixed.

  3. shihyuho commented on Jul 20, 2026

    @shihyuho
    MemberAuthor

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions