Salesforce B2C Commerce Cloud detector pack
Vulkro's B2C Commerce detector pack (PRO-T-B-1) covers Salesforce B2C Commerce Cloud storefronts: the platform that ships under the cartridge model and is the backbone of every SFRA (Storefront Reference Architecture) reference build. It is a separate Salesforce product from the core CRM, with its own language idioms (JavaScript controllers and scripts, ISML templates, OCAPI), its own credential store (the Service Registry), and its own deployment model (cartridge stacks configured in Business Manager).
This page covers the six rules in the pack, how Vulkro detects cartridge codebases, the severity / confidence rubric, and where the findings land in the AppExchange Security Review readiness report.
What it does
When Vulkro scans a project root, it walks the tree once to look
for any file under cartridges/<name>/cartridge/.... If a
cartridge tree is present, the B2C Commerce detector pack runs
over four file kinds:
| Subdir | Extension | Kind |
|---|---|---|
cartridges/<n>/cartridge/controllers/ | .js | Controller |
cartridges/<n>/cartridge/scripts/ | .js, .json | Script |
cartridges/<n>/cartridge/templates/ | .isml | Template |
cartridges/<n>/cartridge/models/ | .js | Model |
Other subdirs under cartridge/ (hooks, experience, forms,
static) are walked but classified as Other; no rule fires on
them by default.
The detector self-filters in microseconds on projects that do not
have a cartridges/ tree, so it adds no overhead to non-Commerce
scans.
Rules
B2C-001: OCAPI authentication missing on a data-modifying endpoint
Severity: High (Confidence: High when no guard is visible in the file, Medium when a guard is chained but the rest of the file is unguarded.)
Fires when an SFRA controller registers a server.post,
server.put, server.patch, or server.delete route without
chaining one of these middleware references:
server.middleware.httpscsrfProtection.validateRequestcsrfProtection.generateTokenuserLoggedIn.validateLoggedInuserLoggedIn.validateLoggedInAjaxsecuredToken(OCAPI)requireLoginconsentTracking.consent
Example (positive)
'use strict';
var server = require('server');
server.post('UpdateProfile', function (req, res, next) {
// no guard - anonymous storefront visitors can hit this
var profile = req.form;
res.json({ ok: true });
next();
});
module.exports = server.exports();
Remediation
Chain the right middleware into the route registration before the handler argument:
var server = require('server');
var csrfProtection = require('*/cartridge/scripts/middleware/csrf');
var userLoggedIn = require('*/cartridge/scripts/middleware/userLoggedIn');
server.post(
'UpdateProfile',
server.middleware.https,
csrfProtection.validateRequest,
userLoggedIn.validateLoggedIn,
function (req, res, next) { /* ... */ }
);
For OCAPI endpoints declare the secured flag in the
ocapi-config.json resource entry.
CWE-306 (Missing Authentication for Critical Function).
B2C-002: Hardcoded payment processor credential
Severity: High (Confidence: High in controllers and scripts,
Medium in models and Other files.)
Fires on two shapes:
- Provider-keyed credential identifier:
adyenApiKey,stripeClientSecret,cybersourceTransactionKey,braintreePrivateKey,paypalClientSecret, and so on with a non-placeholder literal value (length >= 8). - Real-format Stripe key literal:
sk_live_<12+ chars>/sk_test_<12+ chars>/rk_live_<12+ chars>/pk_live_<12+ chars>.
Placeholders (changeme, placeholder, yourkey, yourtoken,
xxxxxxxx, test, example, secret) suppress the finding.
Remediation
Move the credential into the B2C Commerce Service Registry. Store
the value as a password on the service credential record in
Business Manager > Administration > Operations > Services >
Credentials, then read it at runtime:
var LocalServiceRegistry = require('dw/svc/LocalServiceRegistry');
var stripeService = LocalServiceRegistry.createService('stripe.charge', {
createRequest: function (svc, args) {
var apiKey = svc.getConfiguration().getCredential().getPassword();
svc.addHeader('Authorization', 'Bearer ' + apiKey);
return JSON.stringify(args);
}
});
Rotate the leaked credential immediately at the payment processor;
Stripe in particular scans public GitHub and auto-invalidates
sk_live_ keys on discovery.
CWE-798 (Use of Hardcoded Credentials).
B2C-003: Cardholder data flow without PCI-compliant masking
Severity: High (Confidence: High in controllers, Medium elsewhere.)
Fires when a cardholder-data identifier (cardNumber, cardno,
pan, ccnum, cc_number, cvv, cvc, cv2,
securityCode, cardExpiry, cardExpiration, expMonth,
expYear) appears in the same window as a sink call:
- Logger:
Logger.debug/info/warn/error/fatal/trace(...),console.log/info/warn/error(...) - Response:
response.print(...),response.writer,res.send/json/render/write(...) - Persistent:
session.privacy[...],cookie(...),setCookie(...)
Suppressed when the same window contains a maskCard, mask,
maskCardNumber, redact, padStart, substr, substring,
replace, or sanitize call.
Remediation
Mask the value before any sink:
function maskCard(pan) {
if (!pan || pan.length < 4) { return '****'; }
return '****-****-****-' + pan.substr(pan.length - 4);
}
Logger.info('Card: ' + maskCard(order.cardNumber));
Never log CVV or full PAN, ever. The Cartridge platform provides
dw.order.PaymentInstrument.maskedCreditCardNumber for masked
rendering on the read path.
PCI-DSS 3.3 + CWE-311 (Missing Encryption of Sensitive Data).
B2C-004: Unsafe ISML template injection
Severity: Medium (High when reflected from pdict.* or
request.httpParameterMap.*.)
Two shapes:
<isprint value="${expr}" encoding="off"/>- the canonical ISML opt-out of HTML encoding. The merge expression is rendered verbatim, so any HTML or script in the value executes.${expr}inside an<isscript>...</isscript>block. ISML merge expressions are NOT JS-escaped for script context; quotes or</isscript>in the value break out.
The B2C-004 confidence escalates to High when the merge expression reads request input:
pdict.fieldNamepdict[expr]request.httpParameterMap.<x>.valuerequest.httpParameterMap.<x>.stringValue
Suppressed when the expression is wrapped in HTMLENCODE,
JSENCODE, URLENCODE, encodeForHTML, encodeForJavaScript,
encodeURIComponent, or escape.
Remediation
Drop encoding="off" and let ISML HTML-encode the merge by
default. If you must render rich content, wrap the expression in
HTMLENCODE(...):
<isprint value="${HTMLENCODE(pdict.product.description)}"/>
For <isscript> interpolations use JSENCODE:
<isscript>
var keyword = "${JSENCODE(pdict.searchQuery)}";
</isscript>
Better still, move the value into a data-* attribute on a DOM
element and read it from JS rather than inlining the merge in a
script body.
CWE-79 (Cross-Site Scripting).
B2C-005: Service Registry credential leak
Severity: Medium (Confidence: High in controllers and
scripts, Medium / Low in models and Other.)
Two shapes:
- Literal credential passed inline to
LocalServiceRegistry.createService(...)orServiceRegistry.configureService(...). The Service Registry exists exactly so credentials are stored in Business Manager; passing the secret inline defeats that. - File-scope credential-shaped variable assignment in a cartridge
file that does NOT reference
getConfiguration,getCredential,getCredentials,svc.credential,credential.user,credential.password, orcredential.getPasswordanywhere. The file is doing custom credential handling and bypassing the platform's credential vault.
Remediation
Look up credentials at request time:
var paypalService = LocalServiceRegistry.createService('paypal.charge', {
createRequest: function (svc, args) {
var cred = svc.getConfiguration().getCredential();
svc.addHeader('Authorization', 'Bearer ' + cred.getPassword());
return JSON.stringify(args);
}
});
Store the password on the credential record at Business Manager > Administration > Operations > Services > Credentials.
CWE-798 (Use of Hardcoded Credentials).
B2C-006: Customer PII exposure in pipeline calls
Severity: Medium (Confidence: High when the sink is a URL parameter or a controller's response body, Medium for cookie / session.privacy writes.)
Fires when a canonical Cartridge / SFRA customer-data identifier appears in the same window as a sink:
phone,phoneMobile,phoneHome,phoneNumberemail,emailAddress,customerEmaildateOfBirth,birthDate,dobssn,socialSecurityNumber
Sinks:
- URL parameter (
URLUtils.url(...),URLUtils.https(...),URLUtils.abs(...),&name=${...}literals in ISML). - Response body (
response.print(...),response.writer,res.send/json/render(...)). - Cookie /
session.privacy[...]/setCookie(...).
Suppressed when the window contains a redact, hash,
pseudonymise, pseudonymize, mask, anonymize, or
tokenize call.
Remediation
Move the PII out of the sink:
-
For URL params, use an opaque server-side token (look the PII up on the server keyed by the token):
var token = secureRandom();session.privacy.confirmToken = pdict.emailAddress;return URLUtils.https('Account-Confirm', 'token', token); -
For response bodies, redact to the last-4 / domain-only form.
-
For cookies, use the
dw.system.Sessionserver-side store instead of a client cookie.
GDPR Art 32 (Security of Processing) + CCPA reasonable-security + CWE-200 (Information Exposure).
AppExchange Security Review routing
If you generate the AppExchange Security Review readiness report
(vulkro sf-appexchange-report <project>), the B2C-NNN findings
route to these checklist sections:
| Rule | Checklist Section |
|---|---|
| B2C-001 OCAPI / route auth missing | External Integrations and Callouts |
| B2C-002 hardcoded payment credential | Sensitive Data Storage and Logging |
| B2C-003 cardholder data without mask | Sensitive Data Storage and Logging |
| B2C-004 ISML template injection | Lightning Component Security (LWC + Aura) |
| B2C-005 Service Registry credential leak | External Integrations and Callouts |
| B2C-006 customer PII pipeline exposure | External Integrations and Callouts |
The mapping reflects the published Salesforce Partner Community checklist: payment-processor credentials in source and unmasked cardholder data are both "Sensitive Data Storage and Logging" fails; OCAPI / Service Registry / PII over the wire are all "External Integrations" because each crosses the cartridge to external-service boundary. ISML is the B2C storefront's component-rendering surface, so B2C-004 lands under "Lightning Component Security" alongside LWC and Aura.
File path discipline
The detector fires only on files under
cartridges/<name>/cartridge/{controllers,scripts,templates,models}/.
Files outside the cartridge tree (vendor JS, build output, your
SFDX force-app/main/default/ tree) are skipped automatically.
The standard skip list (node_modules, .git, .sfdx, .sf,
target, dist, build) is applied during the walk so a
runaway symlink does not stall the scan.
Where to find it in code
| Concern | Location |
|---|---|
| Detector implementation | src/security/sf_b2c_commerce.rs |
| Scan dispatch | src/security/mod.rs (under the sf_b2c_commerce::has_cartridge_tree check) |
| AppExchange mapping | src/security/appexchange_mapping.rs (RuleSignal::SfB2cCommerce* variants) |
| Fixtures | bench/fixtures/sf_b2c_commerce/{positive,negative}/b2c-NNN/ |
| Integration test | tests/sf_b2c_commerce.rs |