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..ae194067 100644 --- a/pw/pw-csp-nonce/client/src/app/app.component.ts +++ b/pw/pw-csp-nonce/client/src/app/app.component.ts @@ -1,6 +1,7 @@ import { Component } from '@angular/core'; import { Router } from '@angular/router'; import { UserService } from './services/userService'; +import { CspConfig } from './services/cspConfigService'; @Component({ selector: 'app-root', @@ -8,6 +9,59 @@ import { UserService } from './services/userService'; templateUrl: './app.component.html', providers: [], }) + +export class AppComponent { + private csp: string = ""; + private nonce: string = ""; + + constructor( + private router: Router, + public userService: UserService, + public cspConfig: CspConfig) { + + 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(); + } +} + +/* export class AppComponent { constructor(private router: Router, public userService: UserService) {} @@ -15,3 +69,4 @@ export class AppComponent { this.userService.logout(); } } +*/ \ No newline at end of file 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..aeb4cd4c 100644 --- a/pw/pw-csp-nonce/client/src/app/app.module.ts +++ b/pw/pw-csp-nonce/client/src/app/app.module.ts @@ -37,7 +37,7 @@ import { Basket } from './basket/basket'; import { Profile } from './profile/profile'; import { Login } from './login/login'; -// import {CspConfig} from './services/cspConfigService'; +import { CspConfig } from './services/cspConfigService'; @NgModule({ imports: [ @@ -49,6 +49,7 @@ import { Login } from './login/login'; HttpClientModule, ], providers: [ + CspConfig, UserService, BooksService, DataContainerService, @@ -86,4 +87,4 @@ import { Login } from './login/login'; ], bootstrap: [AppComponent], }) -export class AppModule {} +export class AppModule { } 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..2534a4b6 100644 --- a/pw/pw-csp-nonce/client/src/app/services/cspConfigService.ts +++ b/pw/pw-csp-nonce/client/src/app/services/cspConfigService.ts @@ -4,6 +4,34 @@ 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 => { + + this._config = JSON.parse(JSON.stringify(data))['value']; + this._nonce = JSON.parse(JSON.stringify(data))['nonce']; + return data; + }) + } + + get config(): any { + return this._config; + } + + get nonce(): any { + return this._nonce; + } +} \ No newline at end of file diff --git a/pw/pw-csp-nonce/server/pom.xml b/pw/pw-csp-nonce/server/pom.xml index cf2a3b68..6d3fd45e 100644 --- a/pw/pw-csp-nonce/server/pom.xml +++ b/pw/pw-csp-nonce/server/pom.xml @@ -91,6 +91,11 @@ + + io.dropwizard.metrics + metrics-annotation + 4.2.15 + javax.xml.bind jaxb-api diff --git a/pw/pw-csp-nonce/server/src/main/java/com/worldline/bookstore/config/SecurityConfiguration.java b/pw/pw-csp-nonce/server/src/main/java/com/worldline/bookstore/config/SecurityConfiguration.java index 4ce8239d..51d6f467 100644 --- a/pw/pw-csp-nonce/server/src/main/java/com/worldline/bookstore/config/SecurityConfiguration.java +++ b/pw/pw-csp-nonce/server/src/main/java/com/worldline/bookstore/config/SecurityConfiguration.java @@ -105,8 +105,52 @@ protected void configure(HttpSecurity http) throws Exception { ; // TODO uncomment this line to activate JWT filter + // setCspConfig(http); + } + // Add CSP hash for the following inline scripting (see + // https://report-uri.io/home/hash) : + // - "document.write('

Inline scripting is not recommended! But if you + // have not the choice, secure your app with CSP

');" ==> + // 'sha256-lK+Y3vDnNUrD/ZPLGsnM6B+euoBxZ/MyiIbY2G5VoPw=' + // - inline style ... + + /* + * 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(); + * } + */ private JWTConfigurer securityConfigurerAdapter() { return new JWTConfigurer(tokenProvider); } 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..6eab5ba9 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 @@ -2,7 +2,7 @@ public class CSP { - /*private String value; + public String value; private String nonce; public String getNonce() { @@ -29,5 +29,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..c99827ce 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 @@ -16,14 +16,114 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import com.codahale.metrics.annotation.Timed; +//import com.codahale.metrics.annotation.Timed; /** - * REST controller for managing Content-Security-Policy confuguration with random nonce. + * REST controller for managing Content-Security-Policy confuguration with + * random nonce. */ @RestController @RequestMapping("/api") public class CSPResource { + // public CSPwrapper test = CSPwrapper(); + private final Logger log = LoggerFactory.getLogger(CSPResource.class); + + /** Used for Script Nonce */ + private SecureRandom prng = null; + + @GetMapping("/csp") + // 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); + } } + +/* + * package com.worldline.bookstore.web.rest; + * + * import java.security.MessageDigest; + * import java.security.NoSuchAlgorithmException; + * import java.security.SecureRandom; + * import java.util.Collections; + * + * import javax.servlet.http.HttpServletResponse; + * + * import org.slf4j.Logger; + * import org.slf4j.LoggerFactory; + * import org.springframework.http.HttpStatus; + * import org.springframework.http.ResponseEntity; + * import org.springframework.security.crypto.codec.Hex; + * import org.springframework.web.bind.annotation.GetMapping; + * import org.springframework.web.bind.annotation.RequestMapping; + * import org.springframework.web.bind.annotation.RestController; + */ + +// import com.codahale.metrics.annotation.Timed; + +/** + * REST controller for managing Content-Security-Policy confuguration with + * random nonce. + */ +/* + * @RestController + * + * @RequestMapping("/api") + * public class CSPResource { + * + * } + */ \ No newline at end of file