Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions integration-libs/opf/base/root/model/opf-base.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export interface OpfDynamicScript {
cssUrls?: OpfDynamicScriptResource[];
jsUrls?: OpfDynamicScriptResource[];
html?: string;
dynamicContext?: string;
}

export interface OpfKeyValueMap {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,18 +155,119 @@
});
}

executeScriptFromHtml(html: string | undefined) {
executeScriptFromHtml(html: string | undefined, dynamicContext?: string) {
// SSR mode not supported for security concerns
if (!isPlatformServer(this.platformId) && html) {
const element = new DOMParser().parseFromString(html, 'text/html');
const script = element.getElementsByTagName('script');
if (!script?.[0]?.innerText) {
return;
}
Function(script[0].innerText)();

const originalScript = script[0].innerText;
const sessionId = this.generateSessionId();

Check failure

Code scanning / CodeQL

Insecure randomness High

This uses a cryptographically insecure random number generated at
Math.random()
in a security context.

Copilot Autofix

AI 21 days ago

To fix the problem, we should replace the usage of Math.random() in generateSessionId() with a call to a cryptographically secure random number generator. In Node.js and modern browsers, the crypto module provides such functionality. To maintain compatibility with the rest of the code and stay within the shown region, we should import Node's built-in crypto module and use crypto.randomBytes() to generate a random string value for the session ID. The rest of the function can remain unchanged.

In file integration-libs/opf/base/root/services/opf-resource-loader.service.ts, we need to:

  • Add an import for Node's crypto module.
  • Replace line 246 so that instead of using Math.random().toString(36).substring(2), we use a securely generated random string, e.g., from crypto.randomBytes(16).toString('hex').
Suggested changeset 1
integration-libs/opf/base/root/services/opf-resource-loader.service.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/integration-libs/opf/base/root/services/opf-resource-loader.service.ts b/integration-libs/opf/base/root/services/opf-resource-loader.service.ts
--- a/integration-libs/opf/base/root/services/opf-resource-loader.service.ts
+++ b/integration-libs/opf/base/root/services/opf-resource-loader.service.ts
@@ -8,6 +8,7 @@
 import { Injectable, PLATFORM_ID, inject } from '@angular/core';
 import { Config, ScriptLoader } from '@spartacus/core';
 
+import * as crypto from 'crypto';
 import {
   OpfDynamicScriptResource,
   OpfDynamicScriptResourceType,
@@ -243,7 +244,7 @@
    */
   private generateSessionId(): string {
     const timestamp = Date.now();
-    const random = Math.random().toString(36).substring(2);
+    const random = crypto.randomBytes(16).toString('hex');
     return `opf-session-${timestamp}-${random}`;
   }
 
EOF
@@ -8,6 +8,7 @@
import { Injectable, PLATFORM_ID, inject } from '@angular/core';
import { Config, ScriptLoader } from '@spartacus/core';

import * as crypto from 'crypto';
import {
OpfDynamicScriptResource,
OpfDynamicScriptResourceType,
@@ -243,7 +244,7 @@
*/
private generateSessionId(): string {
const timestamp = Date.now();
const random = Math.random().toString(36).substring(2);
const random = crypto.randomBytes(16).toString('hex');
return `opf-session-${timestamp}-${random}`;
}

Copilot is powered by AI and may make mistakes. Always verify output.

let wrappedScript: string;

if (dynamicContext) {
try {
const contextData = JSON.parse(dynamicContext);
wrappedScript = this.createSessionScopedScript(
originalScript,
contextData,
sessionId
);
} catch (error) {
console.warn('Failed to parse dynamic context:', error);
wrappedScript = this.createSecureScript(originalScript, sessionId);
}
} else {
wrappedScript = this.createSecureScript(originalScript, sessionId);
}

this.executeScriptWithSession(wrappedScript, sessionId);
}
}

/**
* Creates session-scoped script with isolated context
* PSP scripts can use OpfContext.orderId, OpfContext.amount, etc. without modifications
*/
private createSessionScopedScript(
originalScript: string,
contextData: any,
sessionId: string
): string {
return `
(function() {
'use strict';
// Session-isolated context (no global pollution)
const OpfContext = ${JSON.stringify(contextData)};
const SessionId = '${sessionId}';
// Context is immediately available - no async waiting
console.log('PSP: Session', SessionId, 'Context:', OpfContext);
// Original PSP script runs with context available immediately
${originalScript}
})();
`;
}

/**
* Creates secure script without context but with session ID
*/
private createSecureScript(
originalScript: string,
sessionId: string
): string {
return `
(function() {
'use strict';
const OpfContext = {};
const SessionId = '${sessionId}';
console.log('PSP: Session', SessionId, 'No context available');
${originalScript}
})();
`;
}

/**
* Generate unique session ID
*/
private generateSessionId(): string {
const timestamp = Date.now();
const random = Math.random().toString(36).substring(2);
return `opf-session-${timestamp}-${random}`;
}

/**
* Execute script with session isolation
*/
private executeScriptWithSession(
scriptContent: string,
sessionId: string
): void {
const scriptElement = this.document.createElement('script');
scriptElement.textContent = scriptContent;
scriptElement.setAttribute('data-session-id', sessionId);
scriptElement.setAttribute('data-opf-script', 'true');
this.document.head.appendChild(scriptElement);

// Clean up after execution
setTimeout(() => {
if (scriptElement.parentNode) {
scriptElement.parentNode.removeChild(scriptElement);
}
}, 100);
}

clearAllResources() {
this.document
.querySelectorAll(`[${this.OPF_RESOURCE_ATTRIBUTE_KEY}]`)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export class OpfCheckoutPaymentWrapperService {
isError: false,
});

protected executeScriptFromHtml(html: string): void {
protected executeScriptFromHtml(html: string, dynamicContext?: string): void {
/**
* Verify first if customer is still on the payment and review page.
* Then execute script extracted from HTML to render payment provider gateway.
Expand All @@ -82,7 +82,10 @@ export class OpfCheckoutPaymentWrapperService {
)
.subscribe(() => {
setTimeout(() => {
this.opfResourceLoaderService.executeScriptFromHtml(html);
this.opfResourceLoaderService.executeScriptFromHtml(
html,
dynamicContext
);
});
});
}
Expand Down Expand Up @@ -172,7 +175,9 @@ export class OpfCheckoutPaymentWrapperService {
renderPaymentGateway(config: OpfPaymentSessionData) {
if (config?.dynamicScript) {
const html = config?.dynamicScript?.html;

let dynamicContext = config?.dynamicScript?.dynamicContext;
dynamicContext =
'{"orderId":"1234","amount":30,"currency":"USD","billingAddress":{"addressLine1":"123 Main St","city":"New York","country":"US"}}';
const paymentOptionId = this.lastPaymentOptionId;

this.opfResourceLoaderService
Expand All @@ -190,7 +195,7 @@ export class OpfCheckoutPaymentWrapperService {
});

if (html) {
this.executeScriptFromHtml(html);
this.executeScriptFromHtml(html, dynamicContext);
}
})
.catch(() => {
Expand Down
212 changes: 212 additions & 0 deletions integration-libs/opf/examples/context-injection-example.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
# OPF Context Injection Implementation

## Overview

This implementation provides a **session-scoped context injection** solution that allows PSP scripts to access backend context data without modifications. PSP scripts can simply use `OpfContext.orderId`, `OpfContext.amount`, etc.


## Implementation

### 1. Spartacus Context Injection

```typescript
// OpfResourceLoaderService
executeScriptFromHtml(html: string | undefined, dynamicContext?: string): void {
if (!isPlatformServer(this.platformId) && html) {
const element = new DOMParser().parseFromString(html, 'text/html');
const script = element.getElementsByTagName('script');

if (!script?.[0]?.innerText) {
return;
}

const originalScript = script[0].innerText;
const sessionId = this.generateSessionId();

let wrappedScript: string;

if (dynamicContext) {
try {
const contextData = JSON.parse(dynamicContext);
wrappedScript = this.createSessionScopedScript(originalScript, contextData, sessionId);
} catch (error) {
console.warn('Failed to parse dynamic context:', error);
wrappedScript = this.createSecureScript(originalScript, sessionId);
}
} else {
wrappedScript = this.createSecureScript(originalScript, sessionId);
}

this.executeScriptWithSession(wrappedScript, sessionId);
}
}
```

### 2. Session-Scoped Script Creation

```typescript
private createSessionScopedScript(originalScript: string, contextData: any, sessionId: string): string {
return `
(function() {
'use strict';

// Session-isolated context (no global pollution)
const OpfContext = ${JSON.stringify(contextData)};
const SessionId = '${sessionId}';

// Context is immediately available - no async waiting
console.log('PSP: Session', SessionId, 'Context:', OpfContext);

// Original PSP script runs with context available immediately
${originalScript}

})();
`;
}
```

### 3. PSP Script Usage

```javascript
// PSP Script - No modifications needed!
(function() {
'use strict';

// Context is immediately available
console.log('PSP: Order ID:', OpfContext.orderId);
console.log('PSP: Amount:', OpfContext.amount);
console.log('PSP: Session:', SessionId);

// Use context data for PSP operations
const paymentData = {
orderId: OpfContext.orderId,
amount: OpfContext.amount || 0,
currency: OpfContext.currency || 'USD',
billingAddress: OpfContext.billingAddress
};

// Initialize PSP with context data
initializePaymentForm(paymentData);

})();
```

## Complete Flow Example

### 1. OPF Backend Response

```json
{
"dynamicScript": {
"dynamicContext": "{\"orderId\":\"12345\",\"billingAddress\":{\"addressLine1\":\"test street\"},\"amount\":99.99,\"currency\":\"USD\"}",
"html": "<script>/* PSP script content */</script>"
}
}
```

### 2. Spartacus Processing

```typescript
// 1. Parse context
const contextData = JSON.parse(dynamicContext);
// contextData = { orderId: "12345", billingAddress: {...}, amount: 99.99, currency: "USD" }

// 2. Generate session ID
const sessionId = "opf-session-1703123456789-abc123";

// 3. Wrap PSP script with context
const wrappedScript = `
(function() {
'use strict';

const OpfContext = ${JSON.stringify(contextData)};
const SessionId = '${sessionId}';

// Original PSP script content
// ... PSP script code ...

})();
`;

// 4. Execute wrapped script
executeScriptWithSession(wrappedScript, sessionId);
```

### 3. PSP Script Execution

```javascript
// PSP script runs with context immediately available
console.log('PSP: Session', SessionId); // "opf-session-1703123456789-abc123"
console.log('PSP: Order ID:', OpfContext.orderId); // "12345"
console.log('PSP: Amount:', OpfContext.amount); // 99.99

// No async waiting, no event listeners needed
initializePaymentForm();
```

## CSP Compliance

### With Nonce Support

```typescript
// CSP-compliant version with nonce
private createNonceSecuredScript(originalScript: string, contextData: any, nonce: string, sessionId: string): string {
return `
(function() {
'use strict';

// Validate nonce for security
if (!'${nonce}' || '${nonce}'.length < 16) {
throw new Error('Invalid nonce provided');
}

// Session-isolated context
const OpfContext = ${JSON.stringify(contextData)};
const SessionId = '${sessionId}';
const Nonce = '${nonce}';

// Original PSP script runs with context available immediately
${originalScript}

})();
`;
}
```

### CSP Headers

```http
Content-Security-Policy:
script-src 'self' 'nonce-{random-nonce}' https://trusted-psp.com;
object-src 'none';
base-uri 'self';
default-src 'self';
```

## Usage

PSP scripts can now simply use:

```javascript
// Access order information
const orderId = OpfContext.orderId;
const amount = OpfContext.amount;
const currency = OpfContext.currency;

// Access billing information
const billingAddress = OpfContext.billingAddress;
const customerEmail = OpfContext.customerEmail;

// Access session information
const sessionId = SessionId;

// Use in PSP operations
const paymentData = {
orderId: OpfContext.orderId,
amount: OpfContext.amount,
currency: OpfContext.currency,
billingAddress: OpfContext.billingAddress
};
```

This approach provides the **easiest integration** for PSP customers while maintaining **maximum security** and **CSP compliance**.
Loading