Skip to main content

LWC and Aura

Vulkro walks every lwc/<name>/<name>.html, lwc/<name>/<name>.js, and aura/<name>/<name>.cmp / <name>Controller.js / <name>Helper.js file in the project. The HTML and template files are parsed for the framework-specific directives (lwc:dom="manual", aura:html, aura:unescapedHtml), and the matching JS controllers are walked for the sinks those directives expose. The detectors cover five of the AppExchange Top-20 entries (9, 10, 11, 12, 17).

What Vulkro detects

  • DOM XSS in LWC: a <template lwc:dom="manual"> block (or any element rendered through lwc:dom="manual") wired to JS that writes element.innerHTML = userInput or uses insertAdjacentHTML(...) without sanitisation. The default LWC template engine escapes interpolated text; lwc:dom="manual" opts out and re-introduces the entire DOM XSS surface.
  • DOM XSS in Aura: <aura:unescapedHtml value="{!v.userInput}" /> in a .cmp file. The aura:unescapedHtml tag bypasses Aura's default escaping; an attacker-controlled attribute renders as HTML.
  • Lightning Web Security policy gaps: components that bypass the LWS sandbox via lightning-permission-set-recommendations, an unrestricted <iframe> to a non-Salesforce origin, or a Lightning Message Channel declared isExposed=true without a documented cross-namespace consumer.
  • CSRF via Aura actions: @AuraEnabled(cacheable=false) Apex methods that perform DML and are reachable from an Aura controller without an Authorization header or origin check. (The cacheable=true shape is read-only and is not flagged.)
  • PostMessage handler trust: window.addEventListener('message', handler) in an LWC or Aura JS file where the handler body does not validate event.origin against an allowlist before acting on event.data.
  • Insecure third-party JS: <script src="..."> referencing a non-Salesforce CDN domain. AppExchange Security Review requires all JS to ship as a StaticResource.
  • Hardcoded secrets in client code: API keys, OAuth tokens, JWT keys, or service-account passwords as JS string literals in .js controller / helper files. Client-side secrets are shipped to every visitor's browser by definition.

Risk anchors

  • AppExchange Top-20 rule 9 (JavaScript not in static resources): the insecure third-party JS detector.
  • AppExchange Top-20 rule 10 (SOQL injection): handled in Apex; the Aura controller side is detected when the action method is reachable from an Aura attribute that flows into a dynamic SOQL string.
  • AppExchange Top-20 rule 11 (Lightning improper CSS load): a <link> element loading a CSS file from outside the component, or an inline <style> block in an Aura .cmp is flagged here.
  • AppExchange Top-20 rule 12 (JavaScript in Salesforce DOM, Classic only): legacy Aura .cmp files that attempt document.querySelector('body') outside the component sandbox.
  • AppExchange Top-20 rule 17 (insecure endpoint): the third-party JS rule plus any LWC fetch('http://...') call.

The DOM XSS shapes also map to OWASP API Security Top 10 entries API1 and API8, and are the client-side half of the Salesforce reflected-XSS class that the Visualforce page covers on the legacy surface.

Example positive (code that triggers a finding)

<!-- inboxPreview.html -->
<template>
<template lwc:dom="manual">
<div class="preview"></div>
</template>
</template>
// inboxPreview.js
import { LightningElement, api } from 'lwc';

export default class InboxPreview extends LightningElement {
@api emailBody;

renderedCallback() {
this.template.querySelector('.preview').innerHTML = this.emailBody;
}
}

The @api attribute lets any parent component pass a string into emailBody; the lwc:dom="manual" template opts out of LWC's default escaping; the renderedCallback writes that string into the DOM via innerHTML. Vulkro emits at High severity because the source is a component public attribute (effectively attacker-reachable through any parent surface).

Example negative (code that does not trigger)

<!-- inboxPreview.html -->
<template>
<template lwc:if={emailBody}>
<div class="preview">{emailBody}</div>
</template>
</template>
// inboxPreview.js
import { LightningElement, api } from 'lwc';

export default class InboxPreview extends LightningElement {
@api emailBody;
}

The template renders emailBody through the LWC engine's default interpolation (which HTML-escapes the value at runtime) and gates the render with lwc:if. There is no lwc:dom="manual" block and no innerHTML write, so no DOM XSS finding emits.

Tuning

  • Confidence is High when the source is @api or a @wire return value and the sink is in the same component file. Medium when the source is read from localStorage or sessionStorage (an attacker who has already executed JS on the origin can populate those). Low when the source is a constant imported from a sibling module.

  • Common false positives: documentation components that demonstrate lwc:dom="manual" with hardcoded HTML. Suppress with:

    // vulkro:disable-next-line LWC_DOM_XSS reason="static demo content only"
    this.template.querySelector('.preview').innerHTML = HELLO_HTML;
  • PostMessage handler trust does not fire if the handler immediately returns when event.origin !== 'https://your-org.lightning.force.com' (or a configurable allowlist).

Where to go next