URL map slug containing `<` or `>` becomes an Elasticsearch range query and serves arbitrary published content instead of 404
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 970
- Forks
- 486
- Avg merge
- 3d 33m
- Merged PRs (30d)
- 170
Description
Problem Statement
Any URL-mapped detail page request whose slug segment contains < or > returns HTTP 200 with an arbitrary published contentlet instead of a 404.
ESUtils.escapeExcludingSlashIncludingSpace escapes every Elasticsearch query_string reserved character except < and > — they are the only two absent from TO_ESCAPE_COLLECTION. URLMapAPIImpl.buildFields interpolates the raw URL slug through that helper into the query, so a request for /blog/<abc> builds:
+Blog.urlTitle_dotRaw:<abc>
Elasticsearch parses field:<term as a less-than range, so the intended exact-slug filter silently becomes:
+Blog.urlTitle_dotRaw:{* TO "abc>"}
— i.e. every published contentlet of that type whose slug sorts alphabetically before the supplied string. getContentlet then requests two hits sorted by score and renders one of them.
Impact
- Non-existent URLs return 200 with real content instead of 404. An unbounded family of URLs serves duplicate content, which is an SEO problem on public sites.
- The contentlet served is not stable: unpublishing the one currently returned causes the same URL to serve a different one.
- Whether a given bound matches anything reveals where published slugs fall alphabetically, so slug names can be discovered without prior knowledge. Content bodies are not exposed — the query retains
+live:true +deleted:falseand the match still passescheckContentPermission.
Affects every URL-mapped content type on every site, in both LIVE and working modes. Reported by a customer whose site crawler surfaced the URLs on their production blog.
Relevant code
| File | Detail |
|---|---|
dotCMS/src/main/java/com/dotcms/content/elasticsearch/util/ESUtils.java:19-24 |
TO_ESCAPE_COLLECTION omits < and > |
dotCMS/src/main/java/com/dotmarketing/cms/urlmap/URLMapAPIImpl.java:244 |
the only production caller of the helper |
dotCMS/src/main/java/com/dotcms/content/elasticsearch/business/ContentFactoryIndexOperationsES.java:444 |
query is executed via QueryBuilders.queryStringQuery |
dotCMS/src/main/java/com/dotmarketing/portlets/structure/StructureUtil.java:18 |
{field} compiles to (.+), so brackets are captured as part of the slug |
The fix is to strip, not escape. Per the Elasticsearch query_string documentation, < and > cannot be backslash-escaped at all — the only remedy is to remove them from the query string. escapeExcludingSlashIncludingSpace is a hand-rolled copy of Lucene's QueryParser.escape(), which omits these two characters for exactly that reason; the gap was inherited.
Why it went unnoticed: ESUtilsTest.test_escapeExcludingSlashIncludingSpace_method re-declares the same hardcoded character set as the production code and loops over it, so it can never detect a character missing from that set.
Not a regression — the escape set is unchanged since #19628 (~2020), so all supported versions including LTS are expected to be affected.
Out of scope (possibly a separate defect): while reproducing this, the rendered detail page appeared to be cached against the resolved contentlet rather than the request URI — after a request to /activities/<sss>/, the legitimate /activities/snowboarding/ served a cached copy whose rel="canonical" pointed at the bogus URL. That may be the page-cache-key issue previously raised in Freshdesk 17099 / 37004. It is deliberately not covered by the acceptance criteria below.
Steps to Reproduce
Reproducible on demo.dotcms.com with no authentication, using the demo Activity type (URL map pattern /activities/{urlTitle}).
- Request a slug that does not exist —
https://demo.dotcms.com/activities/nonexistent/
→ 404, correct. - Request an equally non-existent slug wrapped in angle brackets —
https://demo.dotcms.com/activities/<nonexistent>/
→ 200, renders a published Activity detail page. - Confirm the two characters are the trigger, using square brackets (which are escaped) —
https://demo.dotcms.com/activities/[nonexistent]/
→ 404, correct. - Unpublish the contentlet returned in step 2, then re-request the step 2 URL
→ a different published Activity is served.
Expected: step 2 behaves like step 1 and returns 404.
Actual: step 2 returns 200 with an arbitrary published contentlet.
Acceptance Criteria
-
<and>are removed (not backslash-escaped) from values interpolated into the URL map'squery_stringfilter - A URL-mapped detail page request whose slug contains
<or>and matches no contentlet returns 404 - Valid slugs continue to resolve to the correct contentlet — no regression in URL map resolution
- Slugs containing the other
query_stringreserved characters ([ ] { } + - : ! ( ) ^ " ~ * ? & | \) continue to return 404 without error - Behavior is identical in LIVE and PREVIEW/working modes
- Numeric and float URL map fields (the non-
_dotRawbranch ofbuildFields) are also safe against the same input -
ESUtilsTestno longer re-declares the production character set; it asserts against the documented Elasticsearch reserved-character list so a future omission fails the test - Integration coverage added for a bracketed slug producing no match — e.g. extend
URLMapAPIImplTest
dotCMS Version
Reproduced on demo.dotcms.com (Current Release / dotEvergreen). Customer report is on dotEvergreen Current Release across DEV, STG and Production. The affected code has been unchanged since ~2020, so all supported versions including LTS are expected to be affected.
Severity
Medium - Some functionality impacted
Links
- Freshdesk ticket 38768 — https://dotcms.freshdesk.com/a/tickets/38768
- Related: #33062 (URLMAP_FALLTHROUGH no longer functional) — documents that a non-matching slug is expected to 404
- Possibly related page-cache-key behavior noted above: Freshdesk 17099, Freshdesk 37004
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with ESUtils.escapeExcludingSlashIncludingSpace and ESUtilsTest, then inspect URLMapAPIImpl.buildFields and ContentFactoryIndexOperationsES query execution. Extend the unit and URLMapAPIImpl integration coverage for angle-bracket and other reserved-character slugs, including numeric and float fields and LIVE/working modes. Done means invalid bracketed slugs return 404 while valid URL maps still resolve correctly.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java
- Domain
- backend, search, security, testing-qa
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100