I know this has been asked countless times, but I cant seem to find a solution to my problem.
The Problem : I am unable to use Content-Security-Policy nonce and its generating an error.
The Error via console.log : Refused to apply inline style because it violates the following Content Security Policy directive: "default-src 'self'". Either the 'unsafe-inline' keyword, a hash ('sha256-eM7IckhPhRx5dBGXZhwsgAKulpq/euetK0YPweqUKX4='), or a nonce ('nonce-...') is required to enable inline execution. Note that hashes do not apply to event handlers, style attributes and javascript: navigations unless the 'unsafe-hashes' keyword is present. Note also that 'style-src' was not explicitly set, so 'default-src' is used as a fallback.
What I also tried : I also tried using mod_unique_id instead of using PHP set env but it throws internal server error
What am i doing wrong
My Code :
.htaccess
Options +FollowSymLinks
RewriteEngine On
<IfModule mod_headers.c>
FileETag None
Header unset ETag
Header set Cache-Control "max-age=0, no-cache, no-store, must-revalidate"
Header set Pragma "no-cache"
Header set Expires "Wed, 11 Jan 1984 05:00:00 GMT"
Header set Connection keep-alive
Header set X-XSS-Protection "1; mode=block"
SetEnv MY_CSP_NONCE "<?php echo $_SERVER['MY_CSP_NONCE']; ?>"
Header always set Content-Security-Policy "expr=default-src 'none'; script-src 'self' require-trusted-types-for 'script' https://www.googletagmanager.com https://www.facebook.com https://www.twitter.com https://www.instagram.com 'nonce-%{ENV:MY_CSP_NONCE}' 'strict-dynamic' 'wasm-eval' 'unsafe-eval'; script-src-elem 'self'; connect-src 'self'; img-src 'self' https://storage.googleapis.com data:; video-src 'self' https://storage.googleapis.com data:; style-src 'self' style-src-attr 'self' 'nonce-%{ENV:MY_CSP_NONCE}'; base-uri 'none'; object-src 'none'; frame-ancestors 'self'; frame-src 'self'; sandbox allow-same-origin allow-scripts allow-popups; media-src 'self'; worker-src 'self https://*.cloudflare.com'; manifest-src 'self'; child-src 'self'; prefetch-src 'self' https://storage.googleapis.com https://www.googletagmanager.com; form-action 'self' https://www.paystack.com; font-src 'self' data:; upgrade-insecure-requests"
Header set Feature-Policy "geolocation 'self'; vibrate 'none'"
Header always set Content-Security-Policy-Report-Only "default-src 'self'; report-uri https://www.example.com/csp-report-endpoint"
Header always set X-Frame-Options "sameorigin"
Header set X-Content-Type-Options "nosniff"
Header set Strict-Transport-Security "max-age=15552000; includeSubDomains; preload"
Header always set Cross-Origin-Opener-Policy "same-origin-allow-popups"
Header always set Cross-Origin-Resource-Policy "same-site"
SetEnvIf Referer "^https://storage.googleapis.com" CORP_EXEMPT
Header always set Cross-Origin-Embedder-Policy "require-same-origin"
Header always set Cross-Origin-Embedder-Policy "unsafe-none" env=CORP_EXEMPT
Header set Cross-Origin-Embedder-Policy "unsafe-none" "expr=%{REQUEST_URI} =~ m!\.(png|jpe?g|gif|svg|webp|avif|mp4|webm|m4a|ogv)$!"
</IfModule>
RewriteCond %{SCRIPT_FILENAME} !-d
RewriteCond %{SCRIPT_FILENAME} !-f
RewriteRule ^index$ ./index.php
RewriteRule ^about$ ./about.php
RewriteRule ^404$ ./404.php
RewriteRule ^500$ ./500.php
ErrorDocument 404 https://www.example.com/404
IndexIgnore *
my cookiesetter.php - Where nonce is stored to be included in every script
<?php
$nonce = rtrim(strtr(base64_encode(random_bytes(64)), '+/', '-_'), '=');
putenv("MY_CSP_NONCE=$nonce");
?>
and index.php
<?php include "cookiesetter.php" ?>
<html>
<head>
<title>Example</title>
<style nonce="<?php echo $nonce ?>">
bla bla bla
</style>
</head>
<body>
<script nonce="<?php echo $nonce ?>">
bla bla bla
</script>
</body>
</html>
So i solved it with the help of @soulseekah and another online friend.
NB: You must be using PHP 8.2 version for this to work on live server and localhost. You can change it, for example, on cpanel live server, by searching for "select php version"
NB: You could ask chatgpt to generate the Nginx server version for you, copy and paste the .htaccess and headersettercsp.php, it would do that.
The solution
.htaccess
Options +FollowSymLinks
RewriteEngine On
<IfModule mod_mime.c>
AddType text/css .css
AddType image/png .png
AddType image/jpeg .jpg
AddType image/avif .avif
AddType image/webp .webp
AddType application/font-woff2 .woff2
</IfModule>
<Files "csp_violations.log">
Order Allow,Deny
Deny from all
</Files>
<Files "application_log">
Order Allow,Deny
Deny from all
</Files>
<Files "error_log">
Order Allow,Deny
Deny from all
</Files>
<Files "security_log">
Order Allow,Deny
Deny from all
</Files>
<IfModule mod_headers.c>
FileETag None
Header unset ETag
Header set Cache-Control "public, max-age=240"
Header set Pragma "cache"
Header set Expires "Wed, 11 Jan 1984 05:00:00 GMT"
Header set Connection keep-alive
Header set X-XSS-Protection "1; mode=block"
Header set Referrer-Policy "strict-origin-when-cross-origin"
SetEnvIf Referer "^https://storage.googleapis.com" CORP_EXEMPT
Header always set Cross-Origin-Embedder-Policy "require-same-origin"
Header always set Cross-Origin-Embedder-Policy "unsafe-none" env=CORP_EXEMPT
Header set Cross-Origin-Embedder-Policy "unsafe-none" "expr=%{REQUEST_URI} =~ m!\.(png|jpe?g|gif|svg|webp|avif|mp4|webm|mov|m4a|ogv)$!"
<Files "headersettercsp.php">
<If "-f %{REQUEST_FILENAME}">
SetHandler application/x-httpd-php
Header always set Content-Security-Policy "none"
</If>
</Files>
Header always set Content-Security-Policy-Report-Only "default-src 'self'; report-uri https://www.example.com/csp-report-endpoint"
Header set Feature-Policy "geolocation 'self'"
Header always set X-Frame-Options "SAMEORIGIN"
Header set X-Content-Type-Options "nosniff"
Header set Strict-Transport-Security "max-age=15552000; includeSubDomains; preload"
Header always set Cross-Origin-Opener-Policy "same-origin-allow-popups"
Header always set Cross-Origin-Resource-Policy "cross-origin"
SetEnvIf Origin "https://storage.googleapis.com" CORP_ENABLE
SetEnvIf Origin "https://www.cloudflare.com" CORP_ENABLE
SetEnvIf Origin "https://www.paystack.com" CORP_ENABLE
Header always set Cross-Origin-Resource-Policy "cross-origin" env=CORP_ENABLE
#Header set Access-Control-Expose-Headers "Content-Disposition"
Header set Access-Control-Allow-Methods "GET, HEAD, OPTIONS"
Header set Access-Control-Allow-Headers "Origin, Content-Type, X-Requested-With, Authorization, Accept, x-test-header"
Header merge Vary Origin
RewriteCond %{REQUEST_METHOD} OPTIONS
RewriteRule .* / [R=200,L]
</IfModule>
RewriteCond %{SCRIPT_FILENAME} !-d
RewriteCond %{SCRIPT_FILENAME} !-f
RewriteRule ^index$ ./index.php
RewriteRule ^about$ ./about.php
RewriteRule ^about/(.*)$ ./about.php?linkcheck=$1 [NC,L]
RewriteRule ^400$ ./400.php
RewriteRule ^401$ ./401.php
RewriteRule ^403$ ./403.php
RewriteRule ^404$ ./404.php
RewriteRule ^500$ ./500.php
RewriteRule ^503$ ./503.php
ErrorDocument 400 https://www.example.com/400
ErrorDocument 401 https://www.example.com/401
ErrorDocument 403 https://www.example.com/403
ErrorDocument 404 https://www.example.com/404
ErrorDocument 500 https://www.example.com/500
RewriteCond %{REQUEST_URI} !^/503.php$
RewriteCond %{ENV:REDIRECT_STATUS} 503
# Redirect users to the maintenance page
RewriteRule ^ https://www.example.com/503 [R=301,L]
IndexIgnore *
my headersettercsp.php - Where nonce is stored to be included in every script
$nonce = '';
$charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$charset_length = strlen($charset);
$nonce_length = 16;
for ($i = 0; $i < $nonce_length; $i++) {
$nonce .= $charset[random_int(0, $charset_length - 1)];
}
// Encode the nonce for safe use in URLs
$nonce = base64_encode($nonce);
$nonce = rtrim(strtr($nonce, '+/', '-_'), '=');
$cspHeader = "default-src 'self' data: blob: https://www.example.com/ http://localhost/example http://localhost https://localhost https://www.cloudflare.com/" .
"script-src 'self' 'nonce-$nonce' data: blob: https://www.example.com/js/ http://localhost/example/js; " .
"script-src-attr 'self' " .
"strict-dynamic 'nonce-$nonce' 'wasm-eval'; " .
"connect-src 'self' https://www.example.com; " .
"style-src 'self' 'nonce-$nonce' data: blob: https://www.example.com; " .
"style-src-attr 'self' 'nonce-$nonce'; " .
"base-uri 'none'; " .
"object-src 'none'; " .
"frame-ancestors 'self'; " .
"frame-src 'self' https://www.example.com; " .
"sandbox allow-scripts allow-forms; " .
"img-src 'self' 'nonce-$nonce' data: blob: http://localhost/example https://storage.googleapis.com https://localhost/; " .
"media-src 'self' 'nonce-$nonce' data: blob: http://localhost/example https://storage.googleapis.com https://localhost/; " .
"worker-src 'self' data: blob: https://*.cloudflare.com; " .
"manifest-src 'self' data: blob: https://www.googletagmanager.com https://storage.googleapis.com; " .
"child-src 'self'; " .
"form-action 'self' data: blob: https://www.paystack.com; " .
"font-src 'self' data: blob: https://fonts.gstatic.com; " .
"http://localhost/example/css " .
"https://www.example.com/css " .
"block-all-mixed-content;" .
"upgrade-insecure-requests;" .
"require-trusted-types-for 'script';";
header("Content-Security-Policy: $cspHeader");
// Get all HTTP request headers
$headers = getallheaders();
// Function to perform strict parsing and validation of HTTP headers START
function isValidHeader($header, $value) {
// Check if the header name contains only alphanumeric characters and hyphens
if (!preg_match('/^[a-zA-Z0-9-]+$/', $header)) {
return false;
}
// Check if the header value contains only printable ASCII characters
if (!preg_match('/^[ -~]*$/', $value)) {
// Log the incident
error_log('Header value contains non-printable ASCII characters: ' . $value);
// Optionally, reject the request
// return false;
// Or sanitize the header value
$value = filter_var($value, FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH);
}
// Additional validation rules can be added as needed
return true; // Header is considered valid
}
// Function to perform strict parsing and validation of HTTP headers END
// Iterate through each header START
foreach ($headers as $header => $value) {
// Perform strict parsing and validation
if (!isValidHeader($header, $value)) {
// Reject the request if the header is malformed or suspicious
http_response_code(500);
?>
<script nonce="<?php echo $nonce; ?>"><?php include "includeprefixlink.php" ?>500</script>
<?php
exit();
}
}
// Iterate through each header END
$allowedDomains = [
'https://paystack.com',
'https://www.cloudflare.com',
'https://www.googletagmanager.com',
'https://storage.googleapis.com',
'https://www.example.com',
'http://localhost',
'http://localhost/example',
'http://localhost:8080', // Adjusted to include HTTP for localhost
];
// Initialize a flag to track whether the CORS headers have been set
$corsHeadersSet = false;
foreach ($allowedDomains as $domain) {
$domain = trim($domain);
$sanitizedDomain = filter_var($domain, FILTER_SANITIZE_URL);
if ($sanitizedDomain !== $domain) {
continue; // Reject the domain if the sanitized version is different from the original AND Skip to the next iteration
}
// Check the origin and set CORS headers if a match is found
$origin = isset($_SERVER['HTTP_ORIGIN']) ? $_SERVER['HTTP_ORIGIN'] : '';
//if (!empty($origin) && $origin === $domain) {
// Origin is in the whitelist, allow the request
header('Access-Control-Allow-Origin: ' . $origin);
header('Access-Control-Allow-Credentials: true');
Set the flag to indicate that CORS headers have been set
$corsHeadersSet = true;
break; // Exit the loop early as CORS headers are already set
}
}
//If CORS headers have not been set (no match found in the whitelist), deny the request
if (!$corsHeadersSet) {
http_response_code(403);
?>
<script nonce="<?php echo $nonce; ?>"><?php include "includeprefixlink.php" ?>500</script>-->
exit();
}
helpful resources
https://content-security-policy.com/examples/
AND
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src
this CSP drove me crazy
and index.php and other pages eg about.php
<?php include "headersettercsp.php" ?>
<html>
<head>
<title>Example</title>
<style nonce="<?php echo $nonce ?>">
bla bla bla
</style>
</head>
<body>
<script nonce="<?php echo $nonce ?>">
bla bla bla
</script>
</body>
</html>
Guys please feel free to add an edit what you think. Edit my answer and add yours even if it is in 10 years time.
This is a learning platform.