UNPKG

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
# Salesforce B2C Commerce Cloud: Secure Coding Best Practices 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. ----- ## Core Security Principles - **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(); ```` ### Cross-Site Request Forgery (CSRF) Protection 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... }); ``` ### Server-Side Validation & Output Encoding - **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> ``` ----- ## 2\. Securing OCAPI/SCAPI Hooks 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] ### Authorization Example (`beforePATCH` hook) ```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 }; ``` ### Hook Best Practices - **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 # 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." ``` ## 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); ``` ### Cryptographic Best Practices - **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.