Skip to content

fix(content): generic contentlet deterministic id seed now includes all non-system field values (#36855) - #37274

Open
gortiz-dotcms wants to merge 4 commits into
mainfrom
issue-36855-deterministic-id-generic-contentlet
Open

fix(content): generic contentlet deterministic id seed now includes all non-system field values (#36855)#37274
gortiz-dotcms wants to merge 4 commits into
mainfrom
issue-36855-deterministic-id-generic-contentlet

Conversation

@gortiz-dotcms

Copy link
Copy Markdown
Member

Summary

  • Root cause: DeterministicIdentifierAPIImpl.resolveAssetName(Versionable) fell back to contentlet.getTitle() for all non-Host/FileAsset/HTMLPage/Persona contentlets. Two independently authored contentlets on separate instances with the same type, host, folder, and title received an identical SHA-256 seed — and thus the same deterministic identifier — regardless of all other field values.
  • Fix: When no binary fields are present, the seed is now built from all non-system, non-binary field values sorted by variable name (fieldVar=value|fieldVar2=value2). Two contentlets with the same title but different data produce distinct seeds; two truly identical contentlets still hash to the same identifier, preserving legitimate cross-environment push matching.
  • Existing identifiers are unaffected — the change only applies to newly created contentlets.

Test plan

Closes #36855

🤖 Generated with Claude Code

@claude

claude Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Claude finished @gortiz-dotcms's task in 1m 59s —— View job


🔍 dotCMS Backend Review

Re-reviewed after the latest push (816686be — "escape field-value delimiters and redact seed from debug log"). Both previously flagged 🟡 Medium findings are resolved.

Resolved

  • DeterministicIdentifierAPIImpl.java:232-236 — Raw assetName is no longer logged; the debug line now logs assetNameLength (assetName.map(String::length).orElse(0)), so custom field values no longer leak into DEBUG logs.
  • DeterministicIdentifierAPIImpl.java:136-142 — Delimiters are now escaped (\\\, then |\|, =\=) before joining with |. Since field variable names cannot contain =/|, the concatenated seed is now unambiguous and the crafted-collision vector is closed.

New Issues

  • 🟡 Medium: DeterministicIdentifierAPIImpl.java:131-142 — The seed uses value.toString() for every non-SYSTEM field. Layout/relationship/category/tag/constant/host-folder fields are all DataTypes.SYSTEM and correctly excluded, but KeyValueField, JSONField, and StoryBlockField are LONG_TEXT and thus included.
    • Assumption: for those fields contentlet.get(var) may return a non-String object (e.g. a Map) whose toString() ordering/format is not stable across saves or environments.
    • Why it matters: if toString() is not deterministic, two truly identical contentlets on separate environments could produce different seeds → different deterministic IDs, defeating the cross-environment push-publish matching this PR explicitly promises to preserve.
    • What to verify: confirm contentlet.get() returns the stored String (not a live Map) for KeyValue/JSON/StoryBlock fields, or normalize non-String values to a canonical form before hashing.

Test gaps (non-blocking)

  • 🟡 Medium: DeterministicIdentifierAPITest.java:834+ — The new test covers same-title/different-value and identical-content cases well, but there's no test asserting that field values containing literal | or = (the injection scenario the escaping fixes) produce distinct seeds. Worth one small case: fieldA="a",fieldB="b|c=d" vs fieldA="a|b",fieldB="c=d" must yield different IDs.

Nothing here blocks merge — the escaping and log redaction correctly close the two prior findings.
· branch issue-36855-deterministic-id-generic-contentlet

@github-actions github-actions Bot added the Area : Backend PR changes Java/Maven backend code label Aug 28, 2026
@claude

claude Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

🔍 dotCMS Backend Review

[🟡 Medium] dotCMS/src/main/java/com/dotmarketing/business/DeterministicIdentifierAPIImpl.java:127-137, 223-227

The new fallback path builds seed by concatenating the raw, unhashed value of every non-binary, non-system field on the contentlet. That value is returned as assetName and passed straight into Logger.debug(...) in deterministicIdSeed(...). Before this change, the logged assetName for a generic contentlet was only a binary filename, HTML page URL, persona key tag, or title. Now any custom field value (notes, addresses, emails, internal identifiers — anything an editor types into a text field) flows into this debug log in cleartext. This only manifests when DEBUG logging is enabled for this class (INFO is the typical default), so it's not exposed by default, but it's a real widening of what gets logged.

Logger.debug(DeterministicIdentifierAPIImpl.class,
        String.format(" assetType: %s, assetName: %s,  deterministicId: %s",
                assetType, assetName, deterministicId));

💡 Don't log the raw assetName value — log assetName.map(String::length) or drop it from the format string entirely, relying on the already-hashed deterministicId for debugging.


[🟡 Medium] dotCMS/src/main/java/com/dotmarketing/business/DeterministicIdentifierAPIImpl.java:127-137

Field values are joined with unescaped = and | delimiters. If a field value itself contains | or =, two contentlets with different field-value combinations can produce an identical concatenated seed string, defeating the collision-resistance this change is explicitly meant to provide. A content editor with edit access could deliberately craft field values containing |/= sequences to force a deterministic-ID collision with another contentlet. bestEffortDeterministicId's existing-ID check mitigates a direct overwrite, but the collision still silently defeats the intended per-content uniqueness and produces an unpredictable identifier in push-publish/cross-environment sync scenarios that rely on deterministic IDs to match content.

.map(field -> {
    final Object value = contentlet.get(field.variable());
    return value != null ? field.variable() + "=" + value : null;
})
.filter(Objects::nonNull)
.collect(Collectors.joining("|"));

💡 Escape | and = within each field value (or length-prefix each variable=value pair) before joining, so the seed is unambiguous.


Next steps

  • 🔴 / 🟠 Fix locally and push — these need your judgment
  • 🟡 You can ask me to handle mechanical fixes inline: @claude fix <issue description> in <File.java>
  • Every new push updates this comment automatically

claude[bot]
claude Bot previously approved these changes Aug 28, 2026

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No Critical or High severity issues found.

…g log (#36855)

  Escape backslash, pipe, and equals in field values so crafted values
  cannot produce an identical seed for different contentlets. Replace raw
  assetName in the debug log with its character count to avoid leaking
  field data when DEBUG logging is enabled.

  Refs: #36855

  Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area : Backend PR changes Java/Maven backend code

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

Deterministic identifier for generic contentlets is seeded only on title, causing cross-environment collisions and silent overwrite on push publish

3 participants