Browse Source

feat(core): add content security strategy

pull/3453/head
Arman Ozak 6 years ago
parent
commit
4cc3a272eb
  1. 32
      npm/ng-packs/packages/core/src/lib/strategies/content-security.strategy.ts
  2. 1
      npm/ng-packs/packages/core/src/lib/strategies/index.ts
  3. 27
      npm/ng-packs/packages/core/src/lib/strategies/loading.strategy.ts
  4. 41
      npm/ng-packs/packages/core/src/lib/tests/content-security.strategy.spec.ts
  5. 45
      npm/ng-packs/packages/core/src/lib/tests/lazy-load-utils.spec.ts
  6. 43
      npm/ng-packs/packages/core/src/lib/tests/loading.strategy.spec.ts
  7. 4
      npm/ng-packs/packages/core/src/lib/utils/lazy-load-utils.ts

32
npm/ng-packs/packages/core/src/lib/strategies/content-security.strategy.ts

@ -0,0 +1,32 @@
export abstract class ContentSecurityStrategy {
constructor(public nonce?: string) {}
abstract applyCSP(element: HTMLScriptElement | HTMLStyleElement): void;
}
export class StrictContentSecurityStrategy extends ContentSecurityStrategy {
constructor(nonce: string) {
super(nonce);
}
applyCSP(element: HTMLScriptElement | HTMLStyleElement) {
element.setAttribute('nonce', this.nonce);
}
}
export class LooseContentSecurityStrategy extends ContentSecurityStrategy {
constructor() {
super();
}
applyCSP(_: HTMLScriptElement | HTMLStyleElement) {}
}
export const CONTENT_SECURITY_STRATEGY = {
Loose() {
return new LooseContentSecurityStrategy();
},
Strict(nonce: string) {
return new StrictContentSecurityStrategy(nonce);
},
};

1
npm/ng-packs/packages/core/src/lib/strategies/index.ts

@ -1,3 +1,4 @@
export * from './content-security.strategy';
export * from './cross-origin.strategy';
export * from './dom.strategy';
export * from './loading.strategy';

27
npm/ng-packs/packages/core/src/lib/strategies/loading.strategy.ts

@ -1,6 +1,7 @@
import { Observable, of } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { fromLazyLoad } from '../utils';
import { ContentSecurityStrategy, CONTENT_SECURITY_STRATEGY } from './content-security.strategy';
import { CrossOriginStrategy, CROSS_ORIGIN_STRATEGY } from './cross-origin.strategy';
import { DomStrategy, DOM_STRATEGY } from './dom.strategy';
@ -9,6 +10,7 @@ export abstract class LoadingStrategy<T extends HTMLScriptElement | HTMLLinkElem
public path: string,
protected domStrategy: DomStrategy = DOM_STRATEGY.AppendToHead(),
protected crossOriginStrategy: CrossOriginStrategy = CROSS_ORIGIN_STRATEGY.Anonymous(),
protected contentSecurityStrategy: ContentSecurityStrategy = CONTENT_SECURITY_STRATEGY.Loose(),
) {}
abstract createElement(): T;
@ -16,15 +18,25 @@ export abstract class LoadingStrategy<T extends HTMLScriptElement | HTMLLinkElem
createStream<T extends Event>(): Observable<T> {
return of(null).pipe(
switchMap(() =>
fromLazyLoad<T>(this.createElement(), this.domStrategy, this.crossOriginStrategy),
fromLazyLoad<T>(
this.createElement(),
this.domStrategy,
this.crossOriginStrategy,
this.contentSecurityStrategy,
),
),
);
}
}
export class ScriptLoadingStrategy extends LoadingStrategy<HTMLScriptElement> {
constructor(src: string, domStrategy?: DomStrategy, crossOriginStrategy?: CrossOriginStrategy) {
super(src, domStrategy, crossOriginStrategy);
constructor(
src: string,
domStrategy?: DomStrategy,
crossOriginStrategy?: CrossOriginStrategy,
contentSecurityStrategy?: ContentSecurityStrategy,
) {
super(src, domStrategy, crossOriginStrategy, contentSecurityStrategy);
}
createElement(): HTMLScriptElement {
@ -36,8 +48,13 @@ export class ScriptLoadingStrategy extends LoadingStrategy<HTMLScriptElement> {
}
export class StyleLoadingStrategy extends LoadingStrategy<HTMLLinkElement> {
constructor(href: string, domStrategy?: DomStrategy, crossOriginStrategy?: CrossOriginStrategy) {
super(href, domStrategy, crossOriginStrategy);
constructor(
href: string,
domStrategy?: DomStrategy,
crossOriginStrategy?: CrossOriginStrategy,
contentSecurityStrategy?: ContentSecurityStrategy,
) {
super(href, domStrategy, crossOriginStrategy, contentSecurityStrategy);
}
createElement(): HTMLLinkElement {

41
npm/ng-packs/packages/core/src/lib/tests/content-security.strategy.spec.ts

@ -0,0 +1,41 @@
import {
CONTENT_SECURITY_STRATEGY,
LooseContentSecurityStrategy,
StrictContentSecurityStrategy,
} from '../strategies';
import { uuid } from '../utils';
describe('LooseContentSecurityStrategy', () => {
describe('#applyCSP', () => {
it('should not set nonce attribute', () => {
const strategy = new LooseContentSecurityStrategy();
const element = document.createElement('link');
strategy.applyCSP(element);
expect(element.getAttribute('nonce')).toBeNull();
});
});
});
describe('StrictContentSecurityStrategy', () => {
describe('#applyCSP', () => {
it('should set nonce attribute', () => {
const nonce = uuid();
const strategy = new StrictContentSecurityStrategy(nonce);
const element = document.createElement('link');
strategy.applyCSP(element);
expect(element.getAttribute('nonce')).toBe(nonce);
});
});
});
describe('CONTENT_SECURITY_STRATEGY', () => {
test.each`
name | Strategy | nonce
${'Loose'} | ${LooseContentSecurityStrategy} | ${undefined}
${'Strict'} | ${StrictContentSecurityStrategy} | ${uuid()}
`('should successfully map $name to $Strategy.name', ({ name, Strategy, nonce }) => {
expect(CONTENT_SECURITY_STRATEGY[name](nonce)).toEqual(new Strategy(nonce));
});
});

45
npm/ng-packs/packages/core/src/lib/tests/lazy-load-utils.spec.ts

@ -1,4 +1,9 @@
import { DomStrategy, DOM_STRATEGY } from '../strategies';
import {
ContentSecurityStrategy,
CONTENT_SECURITY_STRATEGY,
DomStrategy,
DOM_STRATEGY,
} from '../strategies';
import { CrossOriginStrategy, CROSS_ORIGIN_STRATEGY } from '../strategies/cross-origin.strategy';
import { uuid } from '../utils';
import { fromLazyLoad } from '../utils/lazy-load-utils';
@ -33,7 +38,15 @@ describe('Lazy Load Utils', () => {
expect(element.crossOrigin).toBe('anonymous');
});
it('should allow setting a crossorigin strategy', () => {
it('should not set integrity by default', () => {
const element = document.createElement('link');
fromLazyLoad(element);
expect(element.getAttribute('integrity')).toBeNull();
});
it('should allow setting a cross-origin strategy', () => {
const element = document.createElement('link');
const integrity = uuid();
@ -44,6 +57,24 @@ describe('Lazy Load Utils', () => {
expect(element.getAttribute('integrity')).toBe(integrity);
});
it('should not set nonce by default', () => {
const element = document.createElement('link');
fromLazyLoad(element);
expect(element.getAttribute('nonce')).toBeNull();
});
it('should allow setting a content security strategy', () => {
const element = document.createElement('link');
const nonce = uuid();
fromLazyLoad(element, undefined, undefined, CONTENT_SECURITY_STRATEGY.Strict(nonce));
expect(element.getAttribute('nonce')).toBe(nonce);
});
it('should emit error event on fail and clear callbacks', done => {
const error = new CustomEvent('error');
const parentNode = { removeChild: jest.fn() };
@ -61,8 +92,11 @@ describe('Lazy Load Utils', () => {
},
} as DomStrategy,
{
setCrossOrigin(el: HTMLLinkElement) {},
setCrossOrigin(_: HTMLLinkElement) {},
} as CrossOriginStrategy,
{
applyCSP(_: HTMLLinkElement) {},
} as ContentSecurityStrategy,
).subscribe({
error: value => {
expect(value).toBe(error);
@ -90,8 +124,11 @@ describe('Lazy Load Utils', () => {
},
} as DomStrategy,
{
setCrossOrigin(el: HTMLLinkElement) {},
setCrossOrigin(_: HTMLLinkElement) {},
} as CrossOriginStrategy,
{
applyCSP(_: HTMLLinkElement) {},
} as ContentSecurityStrategy,
).subscribe({
next: value => {
expect(value).toBe(success);

43
npm/ng-packs/packages/core/src/lib/tests/loading.strategy.spec.ts

@ -1,12 +1,15 @@
import {
CONTENT_SECURITY_STRATEGY,
CROSS_ORIGIN_STRATEGY,
DOM_STRATEGY,
LOADING_STRATEGY,
ScriptLoadingStrategy,
StyleLoadingStrategy,
} from '../strategies';
import { uuid } from '../utils';
const path = 'http://example.com/';
const nonce = uuid();
describe('ScriptLoadingStrategy', () => {
describe('#createElement', () => {
@ -23,17 +26,31 @@ describe('ScriptLoadingStrategy', () => {
it('should use given dom and cross-origin strategies', done => {
const domStrategy = DOM_STRATEGY.PrependToHead();
const crossOriginStrategy = CROSS_ORIGIN_STRATEGY.UseCredentials();
const contentSecurityStrategy = CONTENT_SECURITY_STRATEGY.Strict(nonce);
domStrategy.insertElement = jest.fn((el: HTMLScriptElement) => {
setTimeout(() => {
el.onload(new CustomEvent('success', { detail: el.crossOrigin }));
el.onload(
new CustomEvent('success', {
detail: {
crossOrigin: el.crossOrigin,
nonce: el.getAttribute('nonce'),
},
}),
);
}, 0);
}) as any;
const strategy = new ScriptLoadingStrategy(path, domStrategy, crossOriginStrategy);
const strategy = new ScriptLoadingStrategy(
path,
domStrategy,
crossOriginStrategy,
contentSecurityStrategy,
);
strategy.createStream<CustomEvent>().subscribe(event => {
expect(event.detail).toBe('use-credentials');
expect(event.detail.crossOrigin).toBe('use-credentials');
expect(event.detail.nonce).toBe(nonce);
done();
});
});
@ -56,17 +73,31 @@ describe('StyleLoadingStrategy', () => {
it('should use given dom and cross-origin strategies', done => {
const domStrategy = DOM_STRATEGY.PrependToHead();
const crossOriginStrategy = CROSS_ORIGIN_STRATEGY.UseCredentials();
const contentSecurityStrategy = CONTENT_SECURITY_STRATEGY.Strict(nonce);
domStrategy.insertElement = jest.fn((el: HTMLLinkElement) => {
setTimeout(() => {
el.onload(new CustomEvent('success', { detail: el.crossOrigin }));
el.onload(
new CustomEvent('success', {
detail: {
crossOrigin: el.crossOrigin,
nonce: el.getAttribute('nonce'),
},
}),
);
}, 0);
}) as any;
const strategy = new StyleLoadingStrategy(path, domStrategy, crossOriginStrategy);
const strategy = new StyleLoadingStrategy(
path,
domStrategy,
crossOriginStrategy,
contentSecurityStrategy,
);
strategy.createStream<CustomEvent>().subscribe(event => {
expect(event.detail).toBe('use-credentials');
expect(event.detail.crossOrigin).toBe('use-credentials');
expect(event.detail.nonce).toBe(nonce);
done();
});
});

4
npm/ng-packs/packages/core/src/lib/utils/lazy-load-utils.ts

@ -1,5 +1,7 @@
import { Observable, Observer } from 'rxjs';
import {
ContentSecurityStrategy,
CONTENT_SECURITY_STRATEGY,
CrossOriginStrategy,
CROSS_ORIGIN_STRATEGY,
DomStrategy,
@ -10,8 +12,10 @@ export function fromLazyLoad<T extends Event>(
element: HTMLScriptElement | HTMLLinkElement,
domStrategy: DomStrategy = DOM_STRATEGY.AppendToHead(),
crossOriginStrategy: CrossOriginStrategy = CROSS_ORIGIN_STRATEGY.Anonymous(),
contentSecurityStrategy: ContentSecurityStrategy = CONTENT_SECURITY_STRATEGY.Loose(),
): Observable<T> {
crossOriginStrategy.setCrossOrigin(element);
contentSecurityStrategy.applyCSP(element);
domStrategy.insertElement(element);
return Observable.create((observer: Observer<Event>) => {

Loading…
Cancel
Save