fix: url map 404 on different host (#35268)#35345
fix: url map 404 on different host (#35268)#35345gortiz-dotcms wants to merge 11 commits intomainfrom
Conversation
|
Claude finished @gortiz-dotcms's task in 4m 6s —— View job 🔍 dotCMS Backend Review[🔴 Critical]
if (contentletSearches.isEmpty()) {
contentletSearches =
ContentUtils.pull(this.buildContentQuery(matches, contentType, context, false),
0, 2, "score", this.wuserAPI.getSystemUser(), true);
// No conhost post-filter — any tenant's content can be returned
}💡 After the fallback resolves results, post-filter by owning site: keep only contentlets whose [🟠 High]
final String cacheKey = REQUEST_CACHE_KEY
+ urlMapContext.getUri() + "|" // attacker-controlled
+ urlMapContext.getHost().getIdentifier() + "|"
+ urlMapContext.getLanguageId() + "|"
+ urlMapContext.getMode().name();💡 Use a separator that cannot appear in a URI (e.g., NUL char [🟠 High]
private static final Contentlet CONTENTLET_NOT_FOUND = new Contentlet();
// Contentlet.setInode(), setHost(), setStringProperty(), ... all available💡 Use an opaque, non-Contentlet marker ( [🟡 Medium]
return cached == CONTENTLET_NOT_FOUND ? null : (Contentlet) cached;💡 Add [🟡 Medium]
query.append(" +(conhost:")
.append(context.getHost().getIdentifier()) // not escaped
.append(" OR conhost:")💡 Wrap with [🟡 Medium]
.append(" +" + ESMappingConstants.VARIANT + ":")💡 Replace with Next steps
|
🔍 dotCMS Backend Review[🟠 High]
contentletSearches =
ContentUtils.pull(this.buildContentQuery(matches, contentType, context, false), 0, 2,
"score", this.wuserAPI.getSystemUser(), true);
// No conhost filter → all tenants searched as system user💡 After the fallback resolves a contentlet, post-filter by owning site: verify [🟠 High]
private static final Contentlet CONTENTLET_NOT_FOUND = new Contentlet();
// public setInode(), setLanguageId(), setProperty(), setBinary(), ... all available💡 Use an opaque, non-Contentlet marker: [🟠 High]
import javax.servlet.http.HttpServletRequest;
...
final HttpServletRequest request = HttpServletRequestThreadLocal.INSTANCE.getRequest();💡 Move request-scope caching to the caller (the filter or servlet that invokes [🟡 Medium]
final String cacheKey = REQUEST_CACHE_KEY
+ urlMapContext.getUri() + "|" // URI is attacker-controlled
+ urlMapContext.getHost().getIdentifier() + "|"
+ urlMapContext.getLanguageId() + "|"
+ urlMapContext.getMode().name();💡 Use a separator that cannot appear in a URI (e.g. NUL char is prohibited by RFC 3986), or hash the tuple: Next steps
|
…allback, log hardening - [Critical] Fix getDetailPageUri to fall back to the configured detail-page identifier when the current site has no page at the same path (e.g. detail page lives on a global host). Previously returned Optional.empty(), causing processURLMap to return empty and yield a 404 even when the detail page was accessible. - [High] Add request-scoped cache to getContentlet(UrlMapContext) so that isUrlPattern() and processURLMap(), which are both called on the same HTTP request, share a single ES lookup. Without the cache, each call issued up to 2 ES queries with the cross-site fallback; now the second call reuses the first result via a request attribute. - [Medium] Sanitize the Host header value before writing it to the debug log to prevent log-forging via newline injection (replaceAll on CR/LF/TAB). - [Medium] Change test assertion in processURLMap_contentOnDifferentSite_shouldResolveViaFallback from getName()/title comparison to getIdentifier() for an unambiguous identity check that is not vulnerable to score-ranking non-determinism. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Content types scoped to a specific site may not reliably allow content creation on a different site. The cross-site fallback test now registers the URL-mapped content type on SYSTEM_HOST (idiomatic for cross-site URL mapping), while the content item and the request still come from different sites — so the host-restricted query still misses the content and the fallback is exercised as intended. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
TestDataUtils.getNewsContent uses IndexPolicy.FORCE which writes immediately but does not wait for ES to make the document queryable. The fallback (site-agnostic) query may therefore see no results. Publishing the content triggers IndexPolicy.WAIT_FOR, ensuring the document is committed and searchable before the assertion runs. LIVE mode is used in the UrlMapContext to match the published version. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
Fixes the runtime 404 reported in #35268, which occurs when a URL-mapped content item lives on a different site (siteB) from the one receiving the request (siteA).
What happened
The typical setup:
https://siteA.com/museum_decade/1920).The runtime 404 was caused by
URLMapAPIImpl.buildContentQuery(), which restricted the Elasticsearch query to content on the current host and SYSTEM_HOST:Content created on siteB has
conhost=siteB_id, so it was never found by the query. BecausegetContentlet()returnednull,isUrlPattern()returnedfalse, andprocessURLMap()was never even reached — making the previous fix atgetDetailPageUri()irrelevant for this scenario.What was done
buildContentQuery()now accepts arestrictToHostflag and the content lookup uses a two-step strategy:Once the siteB content is resolved,
getDetailPageUri()correctly returns siteA's configured detail page (since the content type is registered on siteA), and the page renders normally.An integration test (
processURLMap_contentOnDifferentSite_shouldResolveViaFallback) was added toURLMapAPIImplTestto cover this exact scenario.Test plan
URLMapAPIImplTest#processURLMap_contentOnDifferentSite_shouldResolveViaFallback— new test: content on siteB is resolved when requested from siteAURLMapAPIImplTestsuite — all existing tests must continue to passCloses #35268
🤖 Generated with Claude Code