Secure UI Bundles for AgentExchange

UI Bundles run application logic in the user’s browser, introducing client-side security risks. To pass AgentExchange Security Review, follow the rules in this topic to avoid these risks and protect customers.

This guide covers supported frameworks, third-party code validation, and secure coding practices. Apply these rules to your client-side components and supporting Apex controllers.

Submission Gateways and Framework Requirements 

Before your package enters Security Review, you must meet specific requirements. This section covers the required scans, reports, and framework rules.

Required Salesforce Code Analyzer (SFCA) scan 

Salesforce Code Analyzer checks your code for known issues. Before you submit a managed package with UI bundles for AgentExchange Security Review, scan your code with Salesforce Code Analyzer version 5 or higher. Use the uibundle engine plugin. Add the report to your submission. For details, see Salesforce Code Analyzer.

Setup requirements:

  • SFCA version: Use Salesforce Code Analyzer version 5 or higher.
  • Workspace structure and bundle roots: The engine scans your workspace for a dist/ directory and analyzes the compiled output there. It finds a bundle root by looking for a ui-bundle.json file or a *.uibundle-meta.xml file. If it finds neither, it uses any parent directory of dist/. You also need a src/ directory next to dist/ so the engine can run rules that check your source code.
  • No configuration needed: The engine finds the targets on its own, so you don’t have to set anything up.

Framework Versions and Emergency Security Actions 

Older framework versions no longer receive security fixes. Use a current version of React. If a security vulnerability occurs, Salesforce protects customers if you don’t take action.

  • React 18 or higher: Build your React UI bundles with React 18 or higher. Older versions are not actively maintained and lack security patches. Packages using unsupported React versions fail Security Review.
  • package.json file required: Include a valid package.json file that lists all project dependencies.
  • Response to critical framework vulnerabilities: If a critical vulnerability or zero-day CVE affects React or your dependencies, release a patched package within 7 days. If you don’t, Salesforce may disable compromised UI bundles in subscriber orgs to prevent platform-wide vulnerability exploitation.

Front-End Code and Dependency Security 

Because your code runs in the user’s browser, malicious code or data can cause harm. Avoid pulling in external scripts at run time, and sanitize untrusted content.

Managing third-party scripts 

Add all third-party JavaScript into the compiled package during the build process. Don’t load scripts from the internet while the app runs.

Don’t inject scripts dynamically. For example, don’t use document.createElement('script') to pull in code at run time. Loading unverified scripts from a content delivery network (CDN) creates a supply-chain risk. If an attacker changes that external file, they can execute malicious code inside your app.

1// Incorrect Implementation: Loading external scripts at runtime
2import React, { useEffect } from "react";
3
4const ExternalWidget = () => {
5  useEffect(() => {
6    const script = document.createElement("script");
7    script.src = "https://cdn.example.com/library.js";
8    script.async = true;
9    document.body.appendChild(script);
10  }, []);
11  return <div id="external-widget-container" />;
12};

DOM injection and sanitization (cross-site scripting) 

UI bundles run in isolated subdomains (*.my.salesforce.app). A strict Content Security Policy (CSP) limits code execution and data access.

These boundaries protect the main host DOM (*.my.salesforce.com). However, your component code must still render the DOM safely.

Never pass unsanitized input to dynamic rendering targets like dangerouslySetInnerHTML or unchecked URI attributes. Clean dynamic HTML with a trusted library like DOMPurify before rendering it. Also, limit URI attributes to secure https:// endpoints.

1// Incorrect Implementation: Rendering un-sanitized user input directly into DOM targets
2import React from "react";
3
4const CommentView = ({ userHtml, trackingUrl }) => {
5  return (
6    <div>
7      {/* Unsafe DOM injection */}
8      <div dangerouslySetInnerHTML={{ __html: userHtml }} />
9      {/* Unsafe URL protocol binding */}
10      <a href={trackingUrl}>View Details</a>
11    </div>
12  );
13};
1// Correct Implementation: Explicit markup sanitization and protocol verification
2import React from "react";
3import DOMPurify from "dompurify";
4
5const CommentView = ({ userHtml, trackingUrl }) => {
6  // Strips malicious script tags and inline event handlers
7  const cleanHtml = DOMPurify.sanitize(userHtml);
8  // Restricts URI protocol to safe endpoints
9  const isSafeUrl = trackingUrl.startsWith("https://");
10  const safeUrl = isSafeUrl ? trackingUrl : "#";
11  return (
12    <div>
13      <div dangerouslySetInnerHTML={{ __html: cleanHtml }} />
14      <a href={safeUrl} target="_blank" rel="noopener noreferrer">
15        View Details
16      </a>
17    </div>
18  );
19};

State management and event handling 

Ensure a direct user action triggers data changes. Don’t trigger changes from an automatic page load. This practice prevents unexpected updates.

State-changing operations and lifecycle hooks (CSRF protection) 

Don’t run state-changing operations (POST, PUT, DELETE) inside setup hooks like useEffect(). Tie every state change to a user action, such as a button click.

If you put a change() call inside useEffect(), it runs automatically when the page loads or the user switches tabs. This rule prevents unexpected updates that the user didn’t initiate.

1// Incorrect Implementation: Automatic state mutation executed during component lifecycle initialization
2import React, { useEffect } from "react";
3
4const RecordDeleter = ({ recordId }) => {
5  useEffect(() => {
6    // Executes state modification automatically on load without user confirmation
7    fetch("/services/data/v58.0/sobjects/Account/" + recordId, {
8      method: "DELETE",
9      headers: { Authorization: "Bearer ..." },
10    });
11  }, [recordId]);
12  return <div>Processing record...</div>;
13};
1// Correct Implementation: State modification explicitly bound to user interaction event
2import React from "react";
3
4const RecordDeleter = ({ recordId, onDeleteConfirm }) => {
5  const handleDelete = async () => {
6    // Requires explicit user interaction to trigger database mutation
7    await onDeleteConfirm(recordId);
8  };
9  return (
10    <button type="button" onClick={handleDelete} className="slds-button slds-button_destructive">
11      Delete Record
12    </button>
13  );
14};

Backend data access and API security 

Your bundle’s data access methods determine user visibility permissions. This section explains how to access data, when the platform enforces security, and when you must enforce it yourself.

Data access patterns: Salesforce Data SDK vs. custom Apex 

Standard data access (Salesforce Data SDK and UI API) 

For standard data operations, use the Salesforce Data SDK to access the platform’s UI API. The platform automatically enforces record-level sharing, object-level security (CRUD), and field-level security (FLS) based on the user. For details, see User Interface API.

Custom Apex controllers (manual security enforcement) 

Custom Apex runs in system mode by default, meaning standard UI API security guarantees do not apply. As with Lightning Web Components (LWC) or Aura, you must explicitly enforce security controls inside your Apex controllers:

  • Enforce record-level security by declaring classes as with sharing.
  • Enforce FLS and CRUD directly, using WITH USER_MODE.
  • Limit who can run the class by assigning it through granular permission sets tied to the right user personas.

For full details on standard Apex security, see the existing Secure Coding Guidelines.

If you run custom Apex operations without sharing rules, the UI bundle can read or change restricted records.

1// Incorrect Implementation: Custom Apex bypassing sharing rules and field-level permissions (runs in system mode)
2public without sharing class AccountBundleController {
3
4    @AuraEnabled
5    public static List<Account> getAccounts() {
6        // Vulnerable: Executes in system mode, returning records/fields the user shouldn't see
7        return [SELECT Id, Name, Revenue, SSN__c FROM Account];
8    }
9}

Declare custom Apex classes with sharing, and enforce user-mode security on every SOQL and DML operation.

1// Correct Implementation: Custom Apex explicitly enforcing sharing rules and Object/Field Level Security
2public with sharing class AccountBundleController {
3
4    @AuraEnabled(cacheable=true)
5    public static List<Account> getAccounts() {
6        // Enforces Object and Field Level Security at the database layer
7        return [
8            SELECT Id, Name, AnnualRevenue
9            FROM Account
10            WITH USER_MODE
11            LIMIT 100
12        ];
13    }
14}

External backend integrations and web vulnerability scanning 

If your React UI bundle connects to a non-Salesforce endpoint, that application is in the scope of the AgentExchange Security Review process. Run a web vulnerability scan against the external service using OWASP ZAP or Burp Suite. Include the reports in your review package.

For details, see Test Your Entire Solution.

See Also: