Skip to content

Commit

Permalink
fix: Prevent 3rd party script to load ssr (#19443)
Browse files Browse the repository at this point in the history
CXSPA-8080
  • Loading branch information
FollowTheFlo authored Oct 31, 2024
1 parent 64ecb9a commit b0a5081
Show file tree
Hide file tree
Showing 6 changed files with 107 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -321,17 +321,25 @@ describe('OpfResourceLoaderService', () => {
opfResourceLoaderService = TestBed.inject(OpfResourceLoaderService);
});

it('should embed styles with SSR when platform is set to server', fakeAsync(() => {
it('should not loadStyles with SSR when platform is set to server', fakeAsync(() => {
const mockStyleResource = {
url: 'style-url',
type: OpfDynamicScriptResourceType.STYLES,
};

spyOn<any>(opfResourceLoaderService, 'embedStyles').and.callThrough();

spyOn<any>(opfResourceLoaderService, 'loadStyles').and.callThrough();
opfResourceLoaderService.loadProviderResources([], [mockStyleResource]);
expect(opfResourceLoaderService['loadStyles']).not.toHaveBeenCalled();
}));

expect(opfResourceLoaderService['embedStyles']).toHaveBeenCalled();
it('should not loadScript with SSR when platform is set to server', fakeAsync(() => {
const mockScriptResource = {
url: 'script-url',
type: OpfDynamicScriptResourceType.SCRIPT,
};
spyOn<any>(opfResourceLoaderService, 'loadScript').and.callThrough();
opfResourceLoaderService.loadProviderResources([], [mockScriptResource]);
expect(opfResourceLoaderService['loadScript']).not.toHaveBeenCalled();
}));
});

Expand Down Expand Up @@ -369,4 +377,22 @@ describe('OpfResourceLoaderService', () => {
expect(console.log).toHaveBeenCalledWith('Script executed');
});
});

describe('executeHtml in SSR', () => {
it('should not execute script with SSR when platform is set to server', () => {
TestBed.overrideProvider(PLATFORM_ID, { useValue: 'server' });
opfResourceLoaderService = TestBed.inject(OpfResourceLoaderService);

const mockScript = document.createElement('script');
mockScript.innerText = 'console.log("Script executed");';
spyOn(document, 'createElement').and.returnValue(mockScript);
spyOn(console, 'log');

opfResourceLoaderService.executeScriptFromHtml(
'<script>console.log("Script executed");</script>'
);

expect(console.log).not.toHaveBeenCalledWith('Script executed');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,6 @@ export class OpfResourceLoaderService extends ScriptLoader {
}): void {
const { src, callback, errorCallback } = embedOptions;

const isSSR = isPlatformServer(this.platformId);

if (isSSR) {
return;
}

const link: HTMLLinkElement = this.document.createElement('link');
link.href = src;
link.rel = 'stylesheet';
Expand Down Expand Up @@ -138,7 +132,8 @@ export class OpfResourceLoaderService extends ScriptLoader {
}

executeScriptFromHtml(html: string | undefined) {
if (html) {
// 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) {
Expand All @@ -162,6 +157,11 @@ export class OpfResourceLoaderService extends ScriptLoader {
scripts: OpfDynamicScriptResource[] = [],
styles: OpfDynamicScriptResource[] = []
): Promise<void> {
// SSR mode not supported for security concerns
if (isPlatformServer(this.platformId)) {
return Promise.resolve();
}

const resources: OpfDynamicScriptResource[] = [
...scripts.map((script) => ({
...script,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DomSanitizer } from '@angular/platform-browser';
import { WindowRef } from '@spartacus/core';
import { OpfDynamicScript } from '@spartacus/opf/base/root';
import { OpfCtaScriptsService } from '../opf-cta-scripts';
import { OpfCtaElementComponent } from './opf-cta-element.component';
Expand All @@ -9,6 +10,7 @@ describe('OpfCtaButton', () => {
let fixture: ComponentFixture<OpfCtaElementComponent>;
let domSanitizer: DomSanitizer;
let opfCtaScriptsServiceMock: jasmine.SpyObj<OpfCtaScriptsService>;
let windowRef: WindowRef;

const dynamicScriptMock: OpfDynamicScript = {
html: '<div style="border-style: solid;text-align:center;border-radius:10px;align-content:center;background-color:yellow;color:black"><h2>Thanks for purchasing our great products</h2><h3>Please use promo code:<b>123abc</b> for your next purchase<h3></div><script>console.log(\'CTA Script #1 is running\')</script>',
Expand Down Expand Up @@ -44,18 +46,32 @@ describe('OpfCtaButton', () => {
opfCtaScriptsServiceMock.loadAndRunScript.and.returnValue(
Promise.resolve(dynamicScriptMock)
);
windowRef = TestBed.inject(WindowRef);
});

it('should create', () => {
expect(component).toBeTruthy();
});

it('should renderHtml call bypassSecurityTrustHtml', () => {
const html = '<script>console.log("script");</script>';
it('should renderHtml remove script tags and call bypassSecurityTrustHtml', () => {
const html =
'<h1>Test1</h1><script>console.log("script1");</script><h2>Test2</h2><script>console.log("script2");</script>';
const expectedHtml = '<h1>Test1</h1><h2>Test2</h2>';
spyOn(domSanitizer, 'bypassSecurityTrustHtml').and.stub();
component.renderHtml(html);

expect(domSanitizer.bypassSecurityTrustHtml).toHaveBeenCalledWith(html);
expect(domSanitizer.bypassSecurityTrustHtml).toHaveBeenCalledWith(
expectedHtml
);
});

it('should renderHtml not call bypassSecurityTrustHtml in SSR', () => {
spyOn(windowRef, 'isBrowser').and.returnValue(false);
const html = '<h1>Test</h1><script>console.log("script");</script>';
spyOn(domSanitizer, 'bypassSecurityTrustHtml').and.stub();
component.renderHtml(html);

expect(domSanitizer.bypassSecurityTrustHtml).not.toHaveBeenCalledWith(html);
});

it('should call loadAndRunScript', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
inject,
} from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { WindowRef } from '@spartacus/core';
import { OpfDynamicScript } from '@spartacus/opf/base/root';
import { OpfCtaScriptsService } from '../opf-cta-scripts/opf-cta-scripts.service';

Expand All @@ -23,14 +24,26 @@ import { OpfCtaScriptsService } from '../opf-cta-scripts/opf-cta-scripts.service
export class OpfCtaElementComponent implements AfterViewInit {
protected sanitizer = inject(DomSanitizer);
protected opfCtaScriptsService = inject(OpfCtaScriptsService);
loader = true;
protected windowRef = inject(WindowRef);

@Input() ctaScriptHtml: OpfDynamicScript;

ngAfterViewInit(): void {
this.opfCtaScriptsService.loadAndRunScript(this.ctaScriptHtml);
}
renderHtml(html: string): SafeHtml {
return this.sanitizer.bypassSecurityTrustHtml(html);
// Display sanitized html in SSR for security concerns
return this.windowRef.isBrowser()
? this.sanitizer.bypassSecurityTrustHtml(this.removeScriptTags(html))
: html;
}

// Removing script tags on FE until BE fix: CXSPA-8572
protected removeScriptTags(html: string) {
const element = new DOMParser().parseFromString(html, 'text/html');
Array.from(element.getElementsByTagName('script')).forEach((script) => {
html = html.replace(script.outerHTML, '');
});
return html;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,34 @@ describe('OpfGlobalFunctionsService', () => {
expect(service).toBeTruthy();
});

describe('Global Functions in SSR', () => {
const mockPaymentSessionId = 'mockSessionId';
let windowOpf: any;

it('should not register global functions for CHECKOUT in SSR', () => {
spyOn<any>(service, 'registerSubmit').and.callThrough();
spyOn(windowRef, 'isBrowser').and.returnValue(false);
service.registerGlobalFunctions({
domain: GlobalFunctionsDomain.CHECKOUT,
paymentSessionId: mockPaymentSessionId,
vcr: {} as ViewContainerRef,
});
expect(service['registerSubmit']).not.toHaveBeenCalled();
});

it('should not remove global functions for CHECKOUT in SSR', () => {
service.registerGlobalFunctions({
domain: GlobalFunctionsDomain.CHECKOUT,
paymentSessionId: mockPaymentSessionId,
vcr: {} as ViewContainerRef,
});
windowOpf = windowRef.nativeWindow['Opf'];
spyOn(windowRef, 'isBrowser').and.returnValue(false);
service.removeGlobalFunctions(GlobalFunctionsDomain.CHECKOUT);
expect(windowOpf['payments']['checkout']['submit']).toBeDefined();
});
});

describe('should register global functions for CHECKOUT domain', () => {
const mockPaymentSessionId = 'mockSessionId';
let windowOpf: any;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ export class OpfGlobalFunctionsService implements OpfGlobalFunctionsFacade {
vcr,
paramsMap,
}: GlobalFunctionsInput): void {
// SSR not supported
if (!this.winRef.isBrowser()) {
return;
}
switch (domain) {
case GlobalFunctionsDomain.CHECKOUT:
this.registerSubmit(domain, paymentSessionId, vcr);
Expand All @@ -75,6 +79,10 @@ export class OpfGlobalFunctionsService implements OpfGlobalFunctionsFacade {
}

removeGlobalFunctions(domain: GlobalFunctionsDomain): void {
// SSR not supported
if (!this.winRef.isBrowser()) {
return;
}
const window = this.winRef.nativeWindow as any;
if (window?.Opf?.payments[domain]) {
window.Opf.payments[domain] = undefined;
Expand Down

0 comments on commit b0a5081

Please sign in to comment.