dotCMS / dotCMS/core

URL map slug containing `<` or `>` becomes an Elasticsearch range query and serves arbitrary published content instead of 404

Open
#37,033 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

OKR : Customer Support Team : Maintenance Type : Defect
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:false and the match still passes checkContentPermission.

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}).

  1. Request a slug that does not exist — https://demo.dotcms.com/activities/nonexistent/
    404, correct.
  2. Request an equally non-existent slug wrapped in angle brackets — https://demo.dotcms.com/activities/<nonexistent>/
    200, renders a published Activity detail page.
  3. Confirm the two characters are the trigger, using square brackets (which are escaped) — https://demo.dotcms.com/activities/[nonexistent]/
    404, correct.
  4. 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's query_string filter
  • 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_string reserved characters ([ ] { } + - : ! ( ) ^ " ~ * ? & | \) continue to return 404 without error
  • Behavior is identical in LIVE and PREVIEW/working modes
  • Numeric and float URL map fields (the non-_dotRaw branch of buildFields) are also safe against the same input
  • ESUtilsTest no 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.