Skip to main content

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:

SubdirExtensionKind
cartridges/<n>/cartridge/controllers/.jsController
cartridges/<n>/cartridge/scripts/.js, .jsonScript
cartridges/<n>/cartridge/templates/.ismlTemplate
cartridges/<n>/cartridge/models/.jsModel

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.https
  • csrfProtection.validateRequest
  • csrfProtection.generateToken
  • userLoggedIn.validateLoggedIn
  • userLoggedIn.validateLoggedInAjax
  • securedToken (OCAPI)
  • requireLogin
  • consentTracking.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:

  1. Provider-keyed credential identifier: adyenApiKey, stripeClientSecret, cybersourceTransactionKey, braintreePrivateKey, paypalClientSecret, and so on with a non-placeholder literal value (length >= 8).
  2. 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:

  1. <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.
  2. ${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.fieldName
  • pdict[expr]
  • request.httpParameterMap.<x>.value
  • request.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:

  1. Literal credential passed inline to LocalServiceRegistry.createService(...) or ServiceRegistry.configureService(...). The Service Registry exists exactly so credentials are stored in Business Manager; passing the secret inline defeats that.
  2. File-scope credential-shaped variable assignment in a cartridge file that does NOT reference getConfiguration, getCredential, getCredentials, svc.credential, credential.user, credential.password, or credential.getPassword anywhere. 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, phoneNumber
  • email, emailAddress, customerEmail
  • dateOfBirth, birthDate, dob
  • ssn, 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.Session server-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:

RuleChecklist Section
B2C-001 OCAPI / route auth missingExternal Integrations and Callouts
B2C-002 hardcoded payment credentialSensitive Data Storage and Logging
B2C-003 cardholder data without maskSensitive Data Storage and Logging
B2C-004 ISML template injectionLightning Component Security (LWC + Aura)
B2C-005 Service Registry credential leakExternal Integrations and Callouts
B2C-006 customer PII pipeline exposureExternal 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

ConcernLocation
Detector implementationsrc/security/sf_b2c_commerce.rs
Scan dispatchsrc/security/mod.rs (under the sf_b2c_commerce::has_cartridge_tree check)
AppExchange mappingsrc/security/appexchange_mapping.rs (RuleSignal::SfB2cCommerce* variants)
Fixturesbench/fixtures/sf_b2c_commerce/{positive,negative}/b2c-NNN/
Integration testtests/sf_b2c_commerce.rs