Executive Summary#
I identified a critical DOM-based Cross-Site Scripting (XSS) vulnerability in react-show-more-text (v1.7.1 and below). At the time of discovery, this widely used React component for text truncation averaged over 30,000 weekly downloads, representing a significant supply-chain risk to the React ecosystem.
The vulnerability is rooted in an unsafe interaction between layout-measurement logic and HTML restoration. Under specific and realistic layout conditions, attacker-controlled input can bypass React’s escaping model and execute arbitrary JavaScript automatically upon rendering, without additional user interaction (Zero-Click), effectively bypassing the need for specific user engagement with the component.
This research documents a previously unknown vulnerability class in UI helper components: layout-dependent execution paths that silently opt out of framework-level security guarantees.
I have prepared version 1.7.2 to address this issue that includes a security patch.
Vulnerability Risk Assessment: HIGH
- Estimated Severity: 8.1 (CVSS 3.1) / 9.3 (CVSS 4.0)
- Affected Component:
react-show-more-text(Truncate.js) - Vulnerability Type: DOM-based XSS
- Impact: Arbitrary JavaScript execution (Zero-Click under specific layouts)
- Attack Surface: Any application rendering attacker-controlled strings
Impact Demonstration:#
This vulnerability was later chained into a real-world exploitation scenario inside a large enterprise environment, demonstrating credential harvesting, lateral movement, and wormable propagation. A full impact analysis is documented separately:
👉 How a Zero-Day in a React Library Exposed Corporate Data, Employee Credentials, and Financial Assets
Threat Model & Preconditions#
The vulnerability is exploitable when all of the following conditions are met:
- Attacker controls text rendered by
<ShowMoreText />(e.g., comments, profile fields, CMS content) - The content is provided as a string (not pre-sanitized JSX)
- The application does not apply additional server- or client-side sanitization
- The rendered content fits entirely within the component’s layout constraints in a single line
- No restrictive Content Security Policy (CSP) blocks inline event handlers
These conditions are common in real-world React applications, particularly internal tools and content-heavy dashboards.
Technical Deep Dive#
Discovery#
While assessing a web application that displayed user-editable collapsible text, I observed that attacker-controlled HTML was executed rather than rendered as text. Source inspection revealed the use of the react-show-more-text component.
Further analysis showed that the library is a fork of react-show-more, which itself derives truncation logic from react-truncate. A differential review of the forked Truncate.js implementation exposed subtle but security-critical deviations from the original behavior.
Why This Vulnerability Exists#
The root cause is a design decision intended to preserve clickable links during truncation.
Unlike react-truncate, which treats content as plain text, react-show-more-text introduces custom logic to preserve <a> elements using placeholder tokens and string replacement.
A Misleading Abstraction#
Despite its name, react-show-more-text does not operate on text alone. It processes raw HTML strings and ultimately renders them using dangerouslySetInnerHTML.
The public API suggests the component accepts “strings or JSX,” and usage examples exclusively demonstrate plain strings. This creates a false sense of safety, leading developers to assume React’s escaping guarantees still apply.
The Link Placeholder Mechanism#
To preserve links during truncation, detected anchor tags are replaced with placeholders:
<a href="...">Click</a> → [@@@@@=0]@count = length of the anchor text0= index of the stored link
After layout calculation, placeholders are restored to raw <a> tags. This restoration step forces the component to abandon React’s safe text rendering model and rely on dangerouslySetInnerHTML.
No distinction is made between HTML generated by the library and HTML supplied by an attacker.
Exploit Lifecycle#
The exploit survives and executes across three deterministic stages.
Stage 1: Measurement Phase (Off-DOM Layout Calculation)#
To determine truncation boundaries, the component measures rendered text width using an off-screen DOM node:
// Truncate.js (unpatched)
innerText = (node) => {
const div = document.createElement("div");
const content = node.innerHTML.replace(/\r\n|\r|\n/g, " ");
div.innerHTML = this.extractReplaceLinksKeys(content);
return div.textContent;
};React escapes string children at render time, and a payload such as:
'<img src=x onerror=alert(1)>'is rendered as:
<img src=x onerror=alert(1)>Assigning this escaped string to div.innerHTML does not trigger HTML parsing. The payload survives intact as text.
Stage 2: Restoration Phase (Link Rehydration)#
After layout determination, placeholders are restored:
restoreReplacedLinks = (content) => {
this.replacedLinks.forEach(item => {
content = content.replace(item.key, item[0]);
});
return this.createMarkup(content);
};At this stage:
- The payload remains entity-escaped
- No sanitization is applied
- The content is implicitly trusted
Stage 3: Final Render (Execution Sink)#
Execution occurs when the component explicitly opts out of React’s escaping model:
createMarkup = (str) => {
return <span dangerouslySetInnerHTML={{ __html: str }} />;
};The browser decodes HTML entities, parses the markup, and executes attacker-controlled JavaScript.
Data Transformation Trace#
- Input:
<img src=x onerror=alert(1)> - React Render:
<img src=x onerror=alert(1)> - Measurement Phase: Returned unchanged as text
- Final Render: Entities decoded →
<img>parsed → JavaScript executed

Environment-Dependent Exploitation#
The vulnerability is layout-dependent and therefore non-deterministic.
The vulnerable code path is only reached if the content fits entirely within the component’s width. If truncation occurs, the payload is split and execution is neutralized.
This constraint effectively turns exploitation into payload golf: crafting the shortest possible exploit string that fits within strict pixel boundaries.
In practice, this constraint is often trivial due to:
- Wide desktop layouts
- Responsive containers
- Short, attacker-controlled fields defensively wrapped with
<ShowMoreText />(bios, feed items, notifications)
Practical Impact#
In affected applications, this vulnerability enables:
- Session token exfiltration
- Authenticated account takeover
- Credential harvesting in internal dashboards
- Wormable propagation in comment or feed systems
Because execution can occur without user interaction, impact severity is high.
Remediation: Defense in Depth#
I authored a security patch (v1.7.2) applying a defense-in-depth strategy by sanitizing all restored content immediately before the final render.
Patch Highlights:#
- Sanitization: All restored content is sanitized immediately before rendering
- Hardened Defaults: Only
<a>tags and explicitly allowed attributes are permitted
Sanitization is applied directly at the rendering sink to ensure no intermediate transformation reintroduces executable markup.
// Truncate.js (patched)
restoreReplacedLinks = (content) => {
// ... link restoration logic ...
const cleanOutput = DOMPurify.sanitize(content, {
ALLOWED_TAGS: this.props.allowedTags,
ALLOWED_ATTR: this.props.allowedAttributes
});
return this.createMarkup(cleanOutput);
};Disclosure Timeline#
- December 25, 2025: Vulnerability discovered during application testing
- December 28, 2025: Proof-of-concept developed and validated
- January 15, 2026: Disclosure initiated with maintainers
- January 20, 2026: GitHub Security Advisory created; patched version (v1.7.2) prepared pending public release and CVE assignment.
Conclusion#
This vulnerability demonstrates how seemingly benign UI helper libraries can undermine framework-level security guarantees.
Any component that combines:
- layout measurement,
- string manipulation, and
dangerouslySetInnerHTML
operates in a high-risk zone and must assume hostile input by default.
Security boundaries should never depend on layout, measurement, or visual state.
If your application uses react-show-more-text, audit usage carefully and apply sanitization at the final render boundary.




