sfcc-security — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited sfcc-security (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
This document provides a concise guide to security best practices for Salesforce B2C Commerce Cloud development, focusing on SFRA Controllers, OCAPI/SCAPI Hooks, and Custom SCAPI Endpoints.
-----
dw.crypto package for all cryptographic operations. Avoid deprecated Weak* classes like WeakCipher.Content-Security-Policy, X-Frame-Options, and Strict-Transport-Security in the httpHeaders.json file.-----
Controllers are the entry point for storefront logic. Security here is paramount.
Always verify who the user is (authentication) and what they are allowed to do (authorization). There are off course anonymous users, but authenticated users must be verified before accessing protected resources such as the profile, basket, or order history.
userLoggedIn middleware to ensure a shopper is logged in.basket.customerNo matches req.currentCustomer.profile.customerNo).
var server = require('server');
var userLoggedIn = require('\*/cartridge/scripts/middleware/userLoggedIn');
var CustomerMgr = require('dw/customer/CustomerMgr');
// The 'userLoggedIn.validateLoggedIn' middleware handles authentication.
server.post('UpdateProfile', userLoggedIn.validateLoggedIn, function (req, res, next) {
// Authorization MUST be performed inside the controller logic.
var profileForm = server.forms.getForm('profile');
var customer = CustomerMgr.getCustomerByCustomerNumber(
req.currentCustomer.profile.customerNo
);
// Example Authorization Check: Does the logged-in user own this data?
if (customer.profile.email!== profileForm.email.value) {
res.setStatusCode(403);
res.json({ error: 'Forbidden' });
return next();
}
//... proceed with business logic...
res.json({ success: true });
next();
});
module.exports = server.exports();
Use the csrfProtection middleware for any state-changing POST request. [12, 13]
csrfProtection.generateToken when rendering the form page.csrfProtection.validateRequest when processing the form submission.<form action="${URLUtils.url('Account-HandleProfileUpdate')}" method="POST">
...
<input type="hidden" name="${pdict.csrf.tokenName}" value="${pdict.csrf.token}"/>
<button type="submit">Save</button>
</form>// In your controller
var csrfProtection = require('*/cartridge/scripts/middleware/csrf');
// 1. Generate token for the form page
server.get('EditProfile', csrfProtection.generateToken, function(req, res, next) {
//... render page...
});
// 2. Validate token on form submission
server.post('HandleProfileUpdate', csrfProtection.validateRequest, function(req, res, next) {
// If execution reaches here, the token was valid.
//... process form...
});#### CRITICAL: CSRF Middleware Automation
❌ COMMON MISTAKE: Manually adding CSRF tokens to viewData
// ❌ WRONG - Don't do this!
server.get('ShowForm', csrfProtection.generateToken, function(req, res, next) {
res.render('myForm', {
csrf: {
tokenName: req.csrf.tokenName, // ❌ Redundant
token: req.csrf.token // ❌ Redundant
}
});
});✅ CORRECT APPROACH: Let middleware handle it automatically
// ✅ CORRECT - Middleware automatically adds CSRF to pdict
server.get('ShowForm', csrfProtection.generateToken, function(req, res, next) {
res.render('myForm', {
// No need to manually add CSRF - middleware does this
pageTitle: 'My Form',
otherData: 'value'
});
// pdict.csrf.tokenName and pdict.csrf.token are automatically available
});Remote includes (<isinclude url="...">) invoke an entirely NEW request created by the Web Adapter – they DO NOT inherit the authentication context of the parent page. Treat every remote include endpoint as PUBLIC unless you explicitly secure it.
| Risk Vector | Description | Mitigation |
|---|---|---|
| Authentication Bypass | Endpoint renders user‑specific data but no login check runs on include request | Add userLoggedIn.validateLoggedIn AFTER server.middleware.include |
| Sensitive Data in URL | All params are query string → logged, bookmarkable, cache key | Pass only minimal identifiers; never PII, secrets, tokens |
| Cache Poisoning | Unique per-user params (e.g., session IDs) fragment cache & may expose personalized fragments | Keep URL stable; use surrogate keys / vary headers where needed |
| Excessive Fragment Count | Many includes amplify attack surface & performance risk | Keep < 20 per page; consolidate where feasible |
| Nested Includes Depth Abuse | Recursive fragment chains complicate security review & tracing | Avoid nesting beyond depth 2 |
#### Mandatory Middleware Order
server.get('AccountWidget',
server.middleware.include, // 1. Gatekeeper – blocks direct access without include flag
userLoggedIn.validateLoggedIn, // 2. Re-establish authenticated context if user data needed
cache.applyShortPromotionSensitiveCache, // 3. Explicit cache policy (or disable)
function (req, res, next) {
// SAFE: now in controlled context
res.render('components/account/widget');
next();
}
);NEVER put userLoggedIn.validateLoggedIn before server.middleware.include – bots probing the route directly would still trigger authentication logic (unnecessary overhead) and you'd miss the explicit architectural contract.
#### Identifying Remote Include Requests req.includeRequest === true inside the controller. Log defensively:
if (!req.includeRequest) {
// Unexpected direct access attempt
Logger.warn('Blocked direct access to remote include endpoint: {0}', request.httpPath);
res.setStatusCode(404);
return next();
}#### What NOT to Expose via Remote Include
#### Logging & Forensics Use extended request ID format (baseId-depth-index) to correlate parent + fragment in logs. Example: Xa12Bc-0-00 (page), Xa12Bc-1-02 (third fragment). This enables rapid blast-radius analysis during incident response.
#### Security Review Checklist
[ ] server.middleware.include first in chain
[ ] Auth middleware present if user / sensitive data
[ ] No secrets / PII in query params
[ ] Cache TTL appropriate (0 for volatile personal data)
[ ] Fragment count on target pages audited (<20)
[ ] No deep nesting (depth <= 2)
[ ] Logging on unexpected direct access attemptsIf any checklist item fails, remediate before deployment.
#### How CSRF Middleware Works
`csrfProtection.generateToken` automatically:
csrf.tokenName and csrf.token to the response's viewData${pdict.csrf.tokenName} and ${pdict.csrf.token}Templates access tokens directly:
<!-- ✅ Tokens available automatically -->
<input type="hidden" name="${pdict.csrf.tokenName}" value="${pdict.csrf.token}"/>`csrfProtection.validateRequest` automatically:
Key Principle: Never manually add CSRF data to viewData - the middleware handles this completely. Manually adding CSRF tokens is redundant and can lead to inconsistencies or security issues.
mandatory, regexp, max-length) in your form definition XML. SFRA automatically enforces these on the server when the form is processed. [14]encoding="on" in <isprint> tags to prevent XSS. This is the default and should not be turned off without a specific, secure reason. [9]<!-- end list -->
<field formid="email"
type="string"
mandatory="true"
max-length="50"
regexp="^[\w.%+-]+@[\w.-]+\.[\w]{2,6}$"
parse-error="error.message.parse.email" /><div>Your email: <isprint value="${pdict.profileForm.email.value}" encoding="on" /></div>-----
Hooks are powerful but dangerous. They run in a privileged context after initial gateway authentication but before business-level authorization. [15, 16]
Primary Rule: Never trust the request. Always re-validate authorization inside the hook script. Check that the authenticated user owns the object being modified. [16]
beforePATCH hook)'use strict';
var Status = require('dw/system/Status');
var Logger = require('dw/system/Logger');
exports.beforePATCH = function (basket, productItem, productItemDocument) {
// The gateway authenticated the client, but we MUST authorize the action.
if (customer.authenticated) {
// CRITICAL AUTHORIZATION CHECK:
if (basket.customerNo!== customer.profile.customerNo) {
Logger.getLogger('Security').warn('Auth failure: Customer {0} tried to modify basket {1}', customer.profile.customerNo, basket.basketNo);
// Return an error to block the operation.
return new Status(Status.ERROR, 'AUTH_ERROR', 'Request could not be processed.');
}
}
//... additional validation on productItemDocument...
return new Status(Status.OK); // Allow operation
};ProductMgr.getProduct()). [17, 18, 19]try-catch blocks. Return generic dw.system.Status errors to the client. Log detailed, non-sensitive error information for debugging. [16, 20]-----
Custom SCAPI Endpoints use a "contract-first" security model. The OpenAPI Specification (OAS) 3.0 YAML file is an active, enforceable security policy. [21, 22]
The platform validates requests against the OAS contract at the edge, before your script runs. Any request with undefined parameters, headers, or body structures is automatically rejected. [23, 24]
Define security in the OAS contract using securitySchemes and apply them to endpoints. Each endpoint must have exactly one custom scope (prefixed with c_). [21, 25]
<!-- end list -->
openapi: 3.0.0
info:
title: Custom Loyalty API
version: "1.0.0"
servers:
- url: https://{shortCode}.api.commercecloud.salesforce.com
paths:
/c_loyalty/v1/organizations/{organizationId}/shoppers/me/points:
get:
summary: Get Loyalty Points for the current Shopper
operationId: getLoyaltyPointsForShopper
# This endpoint requires a ShopperToken with the 'c_loyalty.read' scope.
security:
- ShopperToken: [c_loyalty.read]
responses:
'200':
description: Success.
/c_loyalty/v1/organizations/{organizationId}/customers/{customerId}/points_adjustment:
post:
summary: Adjust Loyalty Points for a specific customer (Admin Only)
operationId: adjustCustomerLoyaltyPoints
# This endpoint requires an Admin token with the 'c_loyalty.write' scope.
security:
- AmOAuth2: [c_loyalty.write]
responses:
'204':
description: Success.
# Reusable security scheme definitions
components:
securitySchemes:
# Definition for Shopper APIs
ShopperToken:
type: http
scheme: bearer
bearerFormat: JWT
description: "Requires a Shopper Access Token (SLAS) with c_ scopes."
# Definition for Admin APIs
AmOAuth2:
type: oauth2
description: "Requires an Account Manager token with c_ scopes."
flows:
clientCredentials:
tokenUrl: [https://account.demandware.com/dwsso/oauth2/access_token](https://account.demandware.com/dwsso/oauth2/access_token)
scopes:
c_loyalty.read: "Read shopper loyalty data."
c_loyalty.write: "Modify shopper loyalty data."Hardcoding secrets such as API keys, credentials, or encryption keys in source code is a severe vulnerability. The platform provides specific, secure locations for storing different types of secrets:
Administration > Operations > Services). In code, they are accessed as a read-only dw.svc.ServiceCredential object, and their values are never exposed in logs or to the client. This should be the default choice for any third-party integration credential.PASSWORD. The platform automatically encrypts the value of this attribute at rest.ServiceCredential or PASSWORD attributes and should not be the first choice for highly sensitive secrets like private keys or primary authentication credentials.All cryptographic operations must be performed using the APIs provided in the dw.crypto package. This package provides access to industry-standard, Salesforce-maintained cryptographic libraries.
A critical security mandate is to avoid all deprecated Weak* classes, such as WeakCipher, WeakMac, and WeakMessageDigest. These classes use outdated and insecure algorithms that are vulnerable to attack. Some older third-party cartridges may still contain references to them; these cartridges must be updated or replaced.
All new development must use the modern classes like dw.crypto.Cipher. The following is a secure example of symmetric encryption using AES with a GCM block mode, which provides both confidentiality and authenticity.
var Cipher = require('dw/crypto/Cipher');
var Encoding = require('dw/crypto/Encoding');
var SecureRandom = require('dw/crypto/SecureRandom');
// Key must be a securely generated, Base64-encoded 256-bit (32-byte) key.
// It should be stored securely using Service Credentials or an encrypted attribute.
var base64Key = 'YOUR_SECURE_BASE64_ENCODED_KEY_HERE';
// Plaintext to be encrypted
var plainText = 'This is sensitive data.';
// 1. Generate a cryptographically secure random Initialization Vector (IV).
// For AES/GCM, a 12-byte (96-bit) IV is recommended.
var ivBytes = new SecureRandom().nextBytes(12);
var ivBase64 = Encoding.toBase64(ivBytes);
// 2. Encrypt the data using a strong, authenticated encryption algorithm.
var aesGcmCipher = new Cipher();
var encryptedBase64 = aesGcmCipher.encrypt(plainText, base64Key, 'AES/GCM/NoPadding', ivBase64, 0);
// To decrypt, you need the encrypted data, the key, and the same IV.
// var decryptedText = aesGcmCipher.decrypt(encryptedBase64, base64Key, 'AES/GCM/NoPadding', ivBase64, 0);dw.crypto.SecureRandom for generating cryptographically secure random numbers, IVs, and salts.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.