diff --git a/pw/pw-csp-nonce/client/src/app/app.component.ts b/pw/pw-csp-nonce/client/src/app/app.component.ts index 8f99046d..f7bb53e3 100644 --- a/pw/pw-csp-nonce/client/src/app/app.component.ts +++ b/pw/pw-csp-nonce/client/src/app/app.component.ts @@ -1,5 +1,6 @@ import { Component } from '@angular/core'; import { Router } from '@angular/router'; +import { CspConfig } from './services/cspConfigService'; import { UserService } from './services/userService'; @Component({ @@ -9,7 +10,51 @@ import { UserService } from './services/userService'; providers: [], }) export class AppComponent { - constructor(private router: Router, public userService: UserService) {} + private csp: string; + private nonce: string; + + constructor( + private router: Router, + public userService: UserService, + public cspConfig: CspConfig) { + this.csp = this.nonce = ''; + + cspConfig.load().then( + data => { + this.csp = data['value']; + this.nonce = data['nonce']; + + console.debug('csp : ' + this.csp); + console.debug('nonce : ' + this.nonce); + + // can't use the Meta#addTags() method to set CSP because it will insert the meta tag too late, so we add it "manually" + var meta = ""; + this.renderHtml(meta, 'head'); + console.log('content-security-policy meta : ' + meta); + + // Add secure inline scripting (a script block with a nonce) + // The script will just render a message at the bottom of the page + // (here, we don't use document.write method otherwise it will replace the whole page rendering) + var yourHtmlString = + ""; + this.renderHtml(yourHtmlString, 'head'); + console.log('inline scripting !!! ', yourHtmlString); + }); + } + + /** + * + * Renders an html portion inside a given html tag + * @param message: a string which represents the html portion to render in the page + * @param parentTag : the html tag name in which the html portion will be inserted as a first child + */ + private renderHtml(message: string, parentTag: string) { + var fragment = document.createRange().createContextualFragment(message); + document.getElementsByTagName(parentTag)[0].appendChild(fragment); + } logout() { this.userService.logout(); diff --git a/pw/pw-csp-nonce/client/src/app/app.module.ts b/pw/pw-csp-nonce/client/src/app/app.module.ts index 78f979d7..5baa086a 100644 --- a/pw/pw-csp-nonce/client/src/app/app.module.ts +++ b/pw/pw-csp-nonce/client/src/app/app.module.ts @@ -14,6 +14,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { Router } from '@angular/router'; +import { CspConfig } from './services/cspConfigService'; import { UserService } from './services/userService'; import { BooksService } from './services/booksService'; import { DataContainerService } from './services/dataContainerService'; @@ -50,6 +51,7 @@ import { Login } from './login/login'; ], providers: [ UserService, + CspConfig, BooksService, DataContainerService, ContactService, diff --git a/pw/pw-csp-nonce/client/src/app/services/cspConfigService.ts b/pw/pw-csp-nonce/client/src/app/services/cspConfigService.ts index ce133679..64fd5a8d 100644 --- a/pw/pw-csp-nonce/client/src/app/services/cspConfigService.ts +++ b/pw/pw-csp-nonce/client/src/app/services/cspConfigService.ts @@ -3,7 +3,36 @@ import { HttpClient, HttpResponse } from '@angular/common/http'; @Injectable() // This service gets the Content-Security-Policy and a random nonce from a REST api endpoint /api/csp + export class CspConfig { + private _config: any; + private _nonce: any; + private http: HttpClient; + + // can't use classical Angular DI for HttpClient here, because of "cyclic dependency" issues + // Use Injector service to instanciate HttpClient + constructor(injector: Injector) { + this.http = injector.get(HttpClient); + } + + // Load Content-Security-Policy from a REST api endpoint + // The returned data will contain the CSP configuration ('value') and the a random generated nonce ('nonce') + load(): Promise { + return this.http.get('/api/csp') + .toPromise() + .then((data: any) => { + this._config = data['value'] ?? ''; + this._nonce = data['nonce'] ?? ''; + return data; + }) + } + + get config(): any { + return this._config; + } + get nonce(): any { + return this._nonce; + } } diff --git a/pw/pw-csp-nonce/server/pom.xml b/pw/pw-csp-nonce/server/pom.xml index cf2a3b68..a7a38f1e 100644 --- a/pw/pw-csp-nonce/server/pom.xml +++ b/pw/pw-csp-nonce/server/pom.xml @@ -271,6 +271,12 @@ spring-boot-starter-validation + + io.dropwizard.metrics + metrics-annotation + 4.2.15 + + org.springframework.boot spring-boot-starter-web diff --git a/pw/pw-csp-nonce/server/src/main/java/com/worldline/bookstore/web/rest/CSP.java b/pw/pw-csp-nonce/server/src/main/java/com/worldline/bookstore/web/rest/CSP.java index 8b8474eb..d060fc95 100644 --- a/pw/pw-csp-nonce/server/src/main/java/com/worldline/bookstore/web/rest/CSP.java +++ b/pw/pw-csp-nonce/server/src/main/java/com/worldline/bookstore/web/rest/CSP.java @@ -1,8 +1,7 @@ package com.worldline.bookstore.web.rest; public class CSP { - - /*private String value; + private String value; private String nonce; public String getNonce() { @@ -29,5 +28,5 @@ public void setValue(String value) { @Override public String toString() { return "CSP [value=" + value + ", nonce=" + nonce + "]"; - }*/ + } } diff --git a/pw/pw-csp-nonce/server/src/main/java/com/worldline/bookstore/web/rest/CSPResource.java b/pw/pw-csp-nonce/server/src/main/java/com/worldline/bookstore/web/rest/CSPResource.java index 44c10b2d..08e7bccf 100644 --- a/pw/pw-csp-nonce/server/src/main/java/com/worldline/bookstore/web/rest/CSPResource.java +++ b/pw/pw-csp-nonce/server/src/main/java/com/worldline/bookstore/web/rest/CSPResource.java @@ -24,6 +24,64 @@ @RestController @RequestMapping("/api") public class CSPResource { + private final Logger log = LoggerFactory.getLogger(CSPResource.class); - + /** Used for Script Nonce */ + private SecureRandom prng = null; + + @GetMapping("/csp") + @Timed + // Add Script Nonce CSP Policy + public ResponseEntity generateCSP(HttpServletResponse response) { + // --Get its digest + MessageDigest sha; + // --Generate a random number + String randomNum; + try { + this.prng = SecureRandom.getInstance("SHA1PRNG"); + randomNum = new Integer(this.prng.nextInt()).toString(); + sha = MessageDigest.getInstance("SHA-1"); + } + catch (NoSuchAlgorithmException e) { + return new ResponseEntity<>(Collections.singletonMap("CSPException",e.getLocalizedMessage()), HttpStatus.INTERNAL_SERVER_ERROR); + } + + byte[] digest = sha.digest(randomNum.getBytes()); + + // --Encode it into HEXA + char[] scriptNonce = Hex.encode(digest); + + String csp = "script-src" + + " 'unsafe-eval' 'strict-dynamic' " + + " 'nonce-"+String.valueOf(scriptNonce)+"'" + + " 'sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4='" + // SRI hashes for https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js (work only for Chrome) + ";" + + // add connect-src directive to adapt CSP over cross-origin requests (CORS) + "connect-src"+ + " http://localhost:8080 http://localhost:4200 ws://localhost:4200" + + ";"+ + " style-src" + + " 'self' 'unsafe-inline'"+ + ";" + + " font-src" + + " 'self' "+ + ";" + + " img-src" + + " 'self' data:" + + ";" + + " child-src" + + " 'self' " + + ";" + + " object-src" + + " 'none' " + + ";" + + " default-src" + + " 'self' "; + + CSP conf = new CSP(csp); + conf.setNonce(String.valueOf(scriptNonce)); + + log.debug(conf.toString()); + return ResponseEntity.ok(conf); + } } diff --git a/pw/pw-csp/client/src/index.html b/pw/pw-csp/client/src/index.html index 2cdf679a..e5f168c1 100644 --- a/pw/pw-csp/client/src/index.html +++ b/pw/pw-csp/client/src/index.html @@ -1,10 +1,11 @@ - + + Test @@ -18,9 +19,8 @@ Uncomment the line below for PW-CSP solution - This is inline scripting. Not recommended, only for test purpose ! - To secure inline scripting, use CSP 3 sha256 hash syntax : "script-src ... 'sha256-lK+Y3vDnNUrD/ZPLGsnM6B+euoBxZ/MyiIbY2G5VoPw=' - + --> - --> diff --git a/pw/pw-csp/server/src/main/java/com/worldline/bookstore/config/SecurityConfiguration.java b/pw/pw-csp/server/src/main/java/com/worldline/bookstore/config/SecurityConfiguration.java index 7b4343bc..05ee2310 100644 --- a/pw/pw-csp/server/src/main/java/com/worldline/bookstore/config/SecurityConfiguration.java +++ b/pw/pw-csp/server/src/main/java/com/worldline/bookstore/config/SecurityConfiguration.java @@ -105,7 +105,7 @@ protected void configure(HttpSecurity http) throws Exception { ; // TODO uncomment this line to activate JWT filter - // setCspConfig(http); + setCspConfig(http); } @@ -116,34 +116,13 @@ private void setCspConfig(HttpSecurity http) throws Exception { http .headers() .contentSecurityPolicy( - "script-src" + - " 'none' "+ - // "'unsafe-eval' 'unsafe-inline' " + - ";" + - // add connect-src directive to adapt CSP over cross-origin requests (CORS) - "connect-src"+ - " 'self'"+ - ";"+ - " style-src" + - " 'self' 'unsafe-inline'"+ - ";" + - " font-src" + - " 'self' "+ - ";" + - " img-src" + - " 'self' " + - ";" + - " child-src" + - " 'self' " + - ";" + - " object-src" + - " 'none' " + - ";" + - " report-uri" + - " 'http://localhost:4200' " + - ";" + - " default-src" + - " 'self' ");//.reportOnly(); + "default-src 'none';" + + "connect-src 'self';" + + "font-src 'self';" + + "img-src 'self';" + + "style-src 'self' 'unsafe-inline';" + + "script-src 'sha256-lK+Y3vDnNUrD/ZPLGsnM6B+euoBxZ/MyiIbY2G5VoPw=' 'strict-dynamic';"); + //.reportOnly(); } private JWTConfigurer securityConfigurerAdapter() {