sfcc-dev-mcp
Version:
MCP server for Salesforce B2C Commerce Cloud development assistance including logs, debugging, and development tools
275 lines (201 loc) • 13 kB
Markdown
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.
-----
- **Shared Responsibility**: Salesforce secures the cloud infrastructure. You, the developer, are responsible for securing the custom code you write *in* the cloud.
- **Defense-in-Depth**: Security is layered. Do not rely on a single control (like a WAF). Your code must be independently secure.
- **OWASP Top 10**: All development should align with OWASP principles to mitigate common web application vulnerabilities.
- **Server-Side Validation**: Always validate and sanitize all user input on the server. Client-side validation is for user experience only and provides no security. Use an allowlist approach.
- **Secrets Management**: Never hardcode secrets (API keys, credentials). Store them in Custom Site Preferences.
- **Secure Cryptography**: Use the `dw.crypto` package for all cryptographic operations. Avoid deprecated `Weak*` classes like `WeakCipher`.
- **HTTP Security Headers**: Configure security headers like `Content-Security-Policy`, `X-Frame-Options`, and `Strict-Transport-Security` in the `httpHeaders.json` file.
-----
## 1\. Securing SFRA Controllers
Controllers are the entry point for storefront logic. Security here is paramount.
### Authentication & Authorization
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.
- **Authentication**: Use the `userLoggedIn` middleware to ensure a shopper is logged in.
- **Authorization**: After authenticating, verify the user owns the data they are trying to access or modify (e.g., check if `basket.customerNo` matches `req.currentCustomer.profile.customerNo`).
```javascript
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]
1. **Generate Token**: Use `csrfProtection.generateToken` when rendering the form page.
2. **Validate Token**: Use `csrfProtection.validateRequest` when processing the form submission.
```isml
<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>
````
```javascript
// 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...
});
```
- **Validation**: Define validation rules (e.g., `mandatory`, `regexp`, `max-length`) in your form definition XML. SFRA automatically enforces these on the server when the form is processed. [14]
- **Output Encoding**: Always use `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 -->
```xml
<field formid="email"
type="string"
mandatory="true"
max-length="50"
regexp="^[\w.%+-]+@[\w.-]+\.[\w]{2,6}$"
parse-error="error.message.parse.email" />
```
```isml
<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]
```javascript
'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
};
```
- **Performance**: Keep hook logic simple and fast. Avoid making new database calls (e.g., `ProductMgr.getProduct()`). [17, 18, 19]
- **Error Handling**: Wrap logic in `try-catch` blocks. Return generic `dw.system.Status` errors to the client. Log detailed, non-sensitive error information for debugging. [16, 20]
-----
## 3\. Securing Custom SCAPI Endpoints
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]
### Contract-First Security
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]
### Security Schemes & Scopes
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]
- **`ShopperToken`**: For customer-facing APIs. Uses SLAS JWTs. [25]
- **`AmOAuth2`**: For admin/back-office APIs. Uses Account Manager tokens. [25]
<!-- end list -->
```yaml
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
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."
```
## 4\. Advanced Secrets Management
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:
- **Service Credentials (`dw.svc.ServiceCredential`)**: The most secure and appropriate method for storing secrets used to authenticate to external services (e.g., payment gateways, tax providers, shipping services). Credentials are created and managed in Business Manager (`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.
- **Encrypted Custom Object Attributes**: For storing other types of sensitive data that are not service credentials, create a custom attribute on a system or custom object and set its type to `PASSWORD`. The platform automatically encrypts the value of this attribute at rest.
- **Custom Site Preferences**: Suitable for storing non-secret configuration values, such as feature toggles, endpoint URLs, or less sensitive identifiers. While a vast improvement over hardcoding, they are not encrypted in the same way as `ServiceCredential` or `PASSWORD` attributes and should not be the first choice for highly sensitive secrets like private keys or primary authentication credentials.
## 5. Modern Cryptography with dw.crypto
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.
```javascript
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);
```
- **Use Strong Algorithms**: Always use AES with GCM mode for symmetric encryption, RSA with OAEP padding for asymmetric encryption, and SHA-256 or higher for hashing.
- **Generate Secure Random Values**: Use `dw.crypto.SecureRandom` for generating cryptographically secure random numbers, IVs, and salts.
- **Proper Key Management**: Store encryption keys securely using Service Credentials or encrypted custom object attributes. Never hardcode keys in source code.
- **Use Authenticated Encryption**: Prefer AES/GCM mode which provides both confidentiality and authenticity, preventing tampering with encrypted data.
- **Unique IVs**: Always generate a unique, random IV for each encryption operation. Never reuse IVs with the same key.
- **Avoid Weak Classes**: Never use WeakCipher, WeakMac, WeakMessageDigest, or any other deprecated cryptographic classes.