Skip to content

fix(mapper): escape LIKE wildcards and chunk oversized IN clauses - #208

Open
shihyuho wants to merge 2 commits into
jakartafrom
fix/wp4-like-wildcard-escaping
Open

fix(mapper): escape LIKE wildcards and chunk oversized IN clauses#208
shihyuho wants to merge 2 commits into
jakartafrom
fix/wp4-like-wildcard-escaping

Conversation

@shihyuho

Copy link
Copy Markdown
Member

Closes #194

What changed

SEC-01 — LIKE-family wildcard leakage

Like, NotLike, StartingWith and EndingWith concatenated the user value straight into the
LIKE pattern, so a submitted % or _ kept its wildcard meaning. A bare % through a
StartingWith-mapped scoping filter (e.g. a tenant/department prefix) produced like '%%' and
matched every row — a scoping bypass. Values are bound parameters, so this is wildcard-semantics
leakage, not SQL injection.

  • New LikePattern helper in the domain package: escapes \, % and _ in a single pass (the
    escape character itself is handled first by construction, so no double-escaping), and exposes
    LikePattern.ESCAPE_CHAR (\).
  • All four specs now escape the value before composing the pattern and use the escape-aware
    overloads builder.like(expr, pattern, '\\') / builder.notLike(expr, pattern, '\\').
  • Javadoc on each spec updated to state the literal-match guarantee.

MAINT-03 — unbounded IN expansion

In expanded the entire user collection into one IN (...) list; several RDBMS cap the element
count (commonly 1000) and huge lists degrade the query plan.

  • In.MAX_CHUNK_SIZE = 1000 (documented, maintainer-tunable per target RDBMS).
  • Collections larger than the chunk size are partitioned into OR-combined IN predicates.
    Collections at or below it take the previous single-predicate path, so the common case and the
    empty-collection case are unchanged.
  • NotIn inherits this and negates the OR-combined chunks as a whole, which is the correct
    De Morgan equivalent of NOT IN (all).

Release note (behavior change)

Consumers who deliberately passed raw % or _ through LIKE-family fields lose that
(undocumented) behavior.
From this release, values bound to Like, NotLike, StartingWith and
EndingWith are matched literally — wildcards in the value are escaped. Callers that relied on
user- or code-supplied wildcards reaching the database must now build the pattern with their own
specification.

Verification

  • New tests, each asserting through SpecMapper.toSpec:
    • LikeTest.wildcardsMatchLiterally — values containing %, _ and \ each match only their
      literal row.
    • StartingWithTest.wildcardsMatchLiterally — the reported prefix-scope bypass: a bare % now
      matches only the row that literally starts with %, not every row.
    • EndingWithTest.wildcardsMatchLiterally, NotLikeTest.wildcardsMatchLiterally.
    • InTest.moreValuesThanChunkSize / NotInTest.moreValuesThanChunkSize — 1001 values (2 chunks)
      build valid SQL and return the correct rows.
  • Negative check: with the four LIKE-family source files reverted and the new tests kept, all four
    wildcardsMatchLiterally tests fail — they genuinely pin the fix.
  • make test green on both modules (JDK 17, the project's declared target).

Not done

  • No changes outside mapper/src/main/.../domain/ and mapper/src/test/ — the site docs under
    site/content/** that describe the LIKE-family specs are left to the maintainer / release notes.
  • No case-insensitive (IgnoreCase) siblings exist in this family, so nothing further to escape.
  • Chunk size is a source constant, not runtime configuration — the issue asks for a documented
    default the maintainer may tune, not a new config surface.

🤖 Generated with Claude Code

shihyuho and others added 2 commits July 20, 2026 17:12
Values passed to Like, NotLike, StartingWith and EndingWith were
concatenated into the LIKE pattern verbatim, so a submitted % or _ kept
its wildcard meaning: a bare % through a StartingWith-mapped scoping
filter matched every row. Escape the backslash, % and _ via the new
LikePattern helper and declare the escape character on the predicate.

In/NotIn expanded the whole user collection into a single IN clause;
collections larger than In.MAX_CHUNK_SIZE (1000) are now partitioned
into OR-combined chunks.

Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
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.

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

1 participant