Visualforce XSS is one of the oldest findings in Salesforce security reviews, and it keeps appearing because Visualforce protects the common case so well that developers forget the uncommon ones. Standard components encode their output by default. The holes are the places where that default is switched off or does not apply: escape="false", merge fields inside <script> blocks and event handlers, and URLs built from user input.
This post covers each of those contexts, the encoding function that fits each one, and how to fix the patterns that still turn up in older orgs.
How Visualforce encodes output by default
Merge expressions such as {!account.Name} are rendered on the server. Standard components like <apex:outputText> and <apex:outputField> HTML-encode their values, so a stored value of <script>alert(1)</script> appears as text instead of running.
Salesforce's guidance on unescaped output and formulas in Visualforce spells out where that protection ends: output from components with escape="false", and formulas placed outside a Visualforce component, are unfiltered and must be encoded by the developer. The same page notes that because expressions are rendered on the server, you cannot fix this with client-side JavaScript after the fact.
It also makes a point worth repeating: data from inside your own org still needs encoding. A field a portal user, an integration or a web-to-case form can write is attacker input when another user's page renders it. That is stored XSS.
escape="false" on apex:outputText
<!-- Vulnerable: reflected XSS from the URL -->
<apex:outputText value="{!$CurrentPage.parameters.msg}" escape="false"/>
<!-- Vulnerable: stored XSS from a field others can edit -->
<apex:outputText value="{!caseRecord.Description}" escape="false"/>escape="false" is usually added to render some markup: a bold label, a line break, rich text from a field. The fix depends on why:
- If you do not need markup, delete the attribute. The default is
true. - If you need markup you control, build it in the page and put only the variable part through an encoded expression.
- If you need markup from a rich text field, use
<apex:outputField>, which renders rich text area fields the way a record page does, rather thanoutputTextwith escaping off.
<!-- Fixed -->
<apex:outputText value="{!$CurrentPage.parameters.msg}"/>
<b><apex:outputText value="{!caseRecord.Subject}"/></b>The second line puts the fixed markup in the page and leaves the one value that came from data to the default encoding.
Merge fields inside script blocks
This is the most common Visualforce XSS in real code:
<script>
// Vulnerable: a quote in the parameter ends the string
var term = '{!$CurrentPage.parameters.term}';
var owner = '{!account.Owner.Name}';
</script>HTML encoding is the wrong defence here. A value such as ';alert(document.domain);// does not need any angle brackets to break out of a JavaScript string. Use JSENCODE, which escapes quotes, backslashes and other characters that matter in a JavaScript string literal:
<script>
var term = '{!JSENCODE($CurrentPage.parameters.term)}';
var owner = '{!JSENCODE(account.Owner.Name)}';
</script>Keep the merge field inside a quoted string. An unquoted merge field in script context (var n = {!someValue};) has nothing for JSENCODE to protect.
JavaScript inside HTML attributes
Event handler attributes are JavaScript inside HTML, so they need both encodings. Visualforce provides JSINHTMLENCODE, which Salesforce documents as applying HTMLENCODE and then JSENCODE:
<!-- Vulnerable -->
<button onclick="select('{!item.Name}')">Pick</button>
<!-- Fixed -->
<button onclick="select('{!JSINHTMLENCODE(item.Name)}')">Pick</button>Better still, avoid inline handlers: put the value in a data- attribute rendered through a standard component, and read it from a script in a static resource.
Which encoding function goes where?
| Where the value lands | Function |
|---|---|
| HTML body or a quoted attribute, outside a standard component | HTMLENCODE |
Inside a JavaScript string in a <script> block |
JSENCODE |
Inside a JavaScript string in an onclick-style attribute |
JSINHTMLENCODE |
| A query string parameter value | URLENCODE |
URLENCODE makes a value safe as one parameter. It does not make a whole URL safe. If a link or iframe source comes from user input, a javascript: URL or an off-site address is still possible. Validate the scheme and host against an allowlist in the controller, or better, build the URL in Apex from fixed parts.
Fix it in the controller too
Encoding on the page is the last step. The controller is where you can stop bad input earlier:
public with sharing class SearchController {
public String term { get; private set; }
public SearchController() {
String raw = ApexPages.currentPage().getParameters().get('term');
term = String.isBlank(raw) ? '' : raw.abbreviate(100);
}
}Read the parameter once, bound its length, validate its format when there is one (an id, a date, a number), and escape with String.escapeHtml4 or String.escapeEcmaScript when a property will be used in a context the page does not encode. A property that returns a raw request parameter taints every page that renders it.
How much damage can Visualforce XSS do?
Visualforce pages are served from a separate domain from the main Lightning interface, which limits some cross-origin effects. That does not make XSS harmless. Script on the page runs as the viewing user, can call the page's controller actions and remote actions, and can read whatever the page displays. On a public site page, the viewer can be anyone you send a link to; on an internal page, it can be an administrator.
Finding Visualforce XSS with Vulkro
Vulkro for Salesforce reads every .page and .component file and the Apex controllers bound to them. It reports escape="false" on request parameters (High) and on controller properties (Medium), merge fields from the request inside <script> blocks, dynamic <apex:includeScript> and <apex:iframe> sources, and controller properties that return request parameters unescaped. Values wrapped in HTMLENCODE, JSENCODE or JSINHTMLENCODE are not flagged. The Visualforce checks page lists each one.
vulkro-sf scan ./force-appChromeless pages, the shape a public site serves, are listed in the attack-surface inventory as SF-ATTACK-SURFACE-006, so you can review the pages anonymous traffic reaches first.
FAQ
What does escape="false" do in Visualforce?
It turns off HTML encoding for that component's value, so any markup in the value is rendered as HTML. If the value includes user input or a field users can edit, that is cross-site scripting.
When should I use JSENCODE vs HTMLENCODE?
Use JSENCODE for a value inside a JavaScript string in a script block, HTMLENCODE for a value placed in HTML outside a standard component, and JSINHTMLENCODE for JavaScript inside an HTML attribute such as onclick.
Does apex:outputText escape by default?
Yes. escape defaults to true, so <apex:outputText> HTML-encodes its value unless you set escape="false".
Start a 14-day trial from /start/ to scan your Visualforce. Related: Lightning Web Components security and the Experience Cloud guest user checklist.