we have an single-page-application, that uses Spring Boot as backend and React as frontend. We use PrimeReact as our component library for React. We are packaging the React Application as a static part in our Spring Boot Application so that we have only one deployment file and the React Application is served by Spring Boot to the users..
After updating our Node.js-packages PrimeReact injects nonced inline-style at the body element of the resulting pages. The CSP, that is statically created in Spring Boot configuration and added to the HTTP Header bean does not like the nonce and the site looks really unstyled..
Chrome shows this error multiple times:
"Refused to execute inline script because it violates the folowwing Content Security Policy directive: "script src 'self'". Either the 'unsafe-inline' keyword, a has ('sha256-*EXPLICIT-HASH-EVERY-TIME*'), or a nonce ('nonce-...') is required to enable inline execution"
I know, why the browser refuses to use these hashes. But i don't know how to come by with this problem.
Original Packages:
"dependencies": {
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
"@types/node": "^16.18.46",
"@types/react": "^18.2.21",
"@types/react-dom": "^18.2.7",
"chart.js": "^4.4.0",
"chartjs-plugin-datalabels": "^2.2.0",
"http-proxy-middleware": "^2.0.6",
"primeicons": "^6.0.1",
"primereact": "^9.6.2",
"quill": "^1.3.7",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.15.0",
"react-scripts": "5.0.1",
"react-transition-group": "^4.4.5",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"
},
Updated Packages:
"dependencies": {
"@testing-library/jest-dom": "^6.5.0",
"@testing-library/react": "^16.0.1",
"@testing-library/user-event": "^14.5.2",
"@types/jest": "^29.5.12",
"@types/node": "^22.5.4",
"@types/react": "^18.3.5",
"@types/react-dom": "^18.3.0",
"chart.js": "^4.4.4",
"chartjs-plugin-datalabels": "^2.2.0",
"http-proxy-middleware": "^3.0.2",
"primeicons": "^7.0.0",
"primereact": "^10.8.2",
"quill": "^2.0.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.26.1",
"react-scripts": "^5.0.1",
"react-transition-group": "^4.4.5",
"typescript": "^4.9.5",
"web-vitals": "^4.2.3"
},
PrimeReact throws exception at this place (hooks.esm.js) while injecting styles at the HTML-body element:
var load = function load() {
if (!document || isLoaded) {
return;
}
var styleContainer = (context === null || context === void 0 ? void 0 : context.styleContainer) || document.head;
styleRef.current = getCurrentStyleRef(styleContainer);
if (!styleRef.current.isConnected) {
styleRef.current.type = 'text/css';
if (id) {
styleRef.current.id = id;
}
if (media) {
styleRef.current.media = media;
}
DomHandler.addNonce(styleRef.current, context && context.nonce || PrimeReact.nonce);
styleContainer.appendChild(styleRef.current);
if (name) {
styleRef.current.setAttribute('data-primereact-style-id', name);
}
}
styleRef.current.textContent = css;
setIsLoaded(true);
};
The above code was introduced with the new version of PrimeReact, in PrimeReact 9.6.2 this code was not there. I have not found a clue why this may happen now and why.
CSP Generation in Spring Boot:
contentSecurityPolicyConfig -> contentSecurityPolicyConfig.policyDirectives("default-src 'self'; img-src 'self' data:;")
Current unsafe workaround we are using:
contentSecurityPolicyConfig -> contentSecurityPolicyConfig.policyDirectives("default-src 'self' 'unsafe-inline'; img-src 'self' data:;")
I have some questions about this:
Any answer hinting me in the right (and may be best) direction is appreciated. Thanks in advance.
We found a solution:
We are now using a in-HTML CSP. In React we generate a nonce for the current HTML page and pass it to PrimeReact. At a first glance this looked a little bit unsafe: Transfer the content to the client and let the client build the csp but in fact it is only one request that is made for the page and it seals the content so that no other content can be loaded that is not covered by the csp.
const array = new Uint32Array(1);
const nonce = crypto.getRandomValues(array)[0].toString();
function initCSP() {
let csp = "default-src 'self'; img-src 'self' data:; style-src 'self' 'nonce-{$nonce$}'".replace("{$nonce$}", nonce);
var meta = document.createElement('meta');
meta.httpEquiv = "Content-Security-Policy";
meta.content = csp;
let updated = false;
for (const element of document.getElementsByTagName('head')[0].children) {
if (element.getAttribute("http-equiv") === "Content-Security-Policy") {
element.setAttribute("content", csp);
updated = true;
}
}
if (!updated) {
document.getElementsByTagName('head')[0].insertBefore(meta, document.getElementsByTagName('head')[0].firstChild)
}
return nonce;
}
--
const config = {
nonce: nonce
}
--
<PrimeReactProvider value={config}>
...
</PrimeReactProvider>
Some of the code is not yet optimal, but i think you might get the idea.