-
Posts
11,147 -
Joined
-
Last visited
-
Days Won
368
adrian last won the day on February 18
adrian had the most liked content!
Profile Information
-
Gender
Not Telling
-
Location
Canada
Recent Profile Visitors
49,902 profile views
adrian's Achievements
-
Interestingly if I try to open the frontend in Chrome, Firefox, or Zen now, it's still blocked, but it opens in Safari.
-
Thanks for the suggestion, but after deleting the module files and reinstalling them, I have access again for now. But still having issues with the ban duration. I set it to 2 minutes but I am still blocked out (this time just on the frontend). Note that I am logged in, but my admin is not at /processwire or /admin (in case that has any impact on the previous issue where I was actually locked out of the backend).
-
Brave
-
Even after 60 mins I still can't get in and now even my backend admin is blocked. I ended up having to remove the module folder to get access again. What am I doing wrong?
-
adrian started following FieldtypeTimezone - Dynamic Timezone Field with DST Support , WireWall - Advanced Security Firewall Module , $config->cspNonce and 1 other
-
Big thanks for this @maximus A couple of feature suggestions if I may :) Could you change the "Return 404 silently (stealth mode)" option to really be a stealth 404 error because at the moment it still returns the styled black Wirewall page with all its branding - it's just a change to the wording. Any chance of an option to disable the AJAX protection completely? And a confusion - I am logged into my admin, but in the same browser window I have still managed to trigger the rate limit (intentionally), but your docs state "First, all logged-in ProcessWire users are automatically whitelisted." but I am blocked and actually don't seem to be able to remove the block even after deleting the files in /assets/cache/WireWall - what am I missing?
-
@ryan I am wondering if this should be the approach in wire/config.php if (preg_match('#^Content-Security-Policy(?:-Report-Only)?:.*\s(?:script-src|script-src-elem)\s+(?:[^;]+\s)?\'nonce-([\w+/]+=*)\'#mi', implode("\n", headers_list()), $m)) { $config->cspNonce = $m[1]; } else { $config->cspNonce = base64_encode(random_bytes(16)); } This way if we have already set one in htaccess or another included script has set one via a PHP header, then PW would use that instead of defining a new one.
-
Ah yes, of course - that makes sense and a very different use case to what I was thinking about. I suppose if an attacker knew your system they could add that to an injected script, but hopefully that's unlikely.
-
I suppose it would, but wouldn't the process would still invalidate the protection, right? Or am I missing something?
-
@elabx - I don't really know how those apache modules work exactly, but wouldn't they have the same issue as I realized when I thought - why not just hook into Page::render and do a string replace on all <script> tags to add in the nonce at runtime - the problem being of course that this would also add the nonce to any maliciously injected <script> tags, thereby effectively removing all protection?
-
Thanks @elabx - that's a cool idea having apache generate the nonce so you can populate the CSP in htaccess. Can you explain how this works with ProCache though - wouldn't the <script> calls in the cached html files have the version of the nonce from when they were created which would differ from the current one when the page is loaded. I am guessing there is something to this that I am missing - note that I don't really use ProCache. I have been setting my CSP headers in init.php because I want to change them depending on whether I am in the admin or not. And, yes, GTM is an absolute PITA with all their country TLDs. At the moment I have that part of the policy in report-only mode and I am adding domains as they are reported. $isAdmin = isset($_SERVER['REQUEST_URI']) && strpos($_SERVER['REQUEST_URI'], $this->wire('config')->urls->admin) === 0; // add to list as we observe more violations and determine which sources we need to allow $googleDomains = [ "https://www.google.ca", "https://www.google.co.in", "https://www.google.co.nz", "https://www.google.co.uk", "https://www.google.com", "https://www.google.com.au", "https://www.google.com.br", "https://www.google.com.my", "https://www.google.com.sg", "https://www.google.de", "https://www.google.fr", "https://www.google.lu", "https://www.google.md", "https://www.google.nl", "https://www.google.se", "https://*.googletagmanager.com", "https://*.google-analytics.com", "https://*.googleadservices.com", "https://*.googlesyndication.com", "https://*.doubleclick.net" ]; $imgSources = array_merge( [ "'self'", "data:", "https://cdn.jsdelivr.net", "https://i.vimeocdn.com", "https://www.facebook.com", "https://connect.facebook.net", "https://www.gravatar.com" ], $googleDomains ); $connectSources = array_merge( [ "'self'", "https://svc.webspellchecker.net", "https://www.facebook.com", "https://connect.facebook.net", "https://*.facebook.com", "https://*.facebook.net" ], $googleDomains ); $scriptSourcesElem = [ "'self'", "'unsafe-eval'", // Required for Vue 2 template compilation ]; // Admin needs unsafe-inline, frontend uses nonces if ($isAdmin) { $scriptSourcesElem[] = "'unsafe-inline'"; } else { $scriptSourcesElem[] = "'nonce-" . $config->cspNonce . "'"; } $scriptSourcesElem = array_merge($scriptSourcesElem, [ "https://cdn.jsdelivr.net", "https://cdn.paddle.com", "https://www.googletagmanager.com", "https://www.google-analytics.com", "https://googleads.g.doubleclick.net", "https://connect.facebook.net", "https://svc.webspellchecker.net" ]); // For inline event handlers (onclick, onload, etc.) - allow them $scriptSourcesAttr = [ "'unsafe-inline'" // Allow onclick/onload/etc handlers ]; $styleSources = [ "'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://fonts.googleapis.com", "https://svc.webspellchecker.net" ]; $fontSources = [ "'self'", "data:", "https://fonts.gstatic.com", "https://svc.webspellchecker.net" ]; // ENFORCED CSP (blocks violations) $cspEnforce = [ "object-src 'none'", "base-uri 'none'", "form-action 'self' https://www.facebook.com", "frame-ancestors 'self'", "report-uri https://mysite.com/csp-report/", "report-to csp-endpoint" ]; if($config->https) { $cspEnforce[] = "upgrade-insecure-requests"; } // REPORT-ONLY CSP (just reports, doesn't block) $cspReportOnly = [ "script-src-elem " . implode(' ', $scriptSourcesElem), // Controls <script> tags "script-src-attr " . implode(' ', $scriptSourcesAttr), // Controls onclick/onload/etc "style-src " . implode(' ', $styleSources), "font-src " . implode(' ', $fontSources), "img-src " . implode(' ', $imgSources), "connect-src " . implode(' ', $connectSources), "report-uri https://mysite.com/csp-report/", "report-to csp-endpoint" ]; header("Reporting-Endpoints: csp-endpoint=\"https://mysite.com/csp-report/\""); header("Content-Security-Policy: " . implode('; ', $cspEnforce)); header("Content-Security-Policy-Report-Only: " . implode('; ', $cspReportOnly)); And in case anyone is interested, I have this script for the csp-report template file: <?php namespace ProcessWire; $data = file_get_contents('php://input'); if($data) { $report = json_decode($data, true); // Browser extension indicators - these are SAFE to ignore $extensionIndicators = [ 'chrome-extension://', 'moz-extension://', 'safari-extension://', 'webkit-masked-url://', 'ms-browser-extension://' ]; // Known bad actor domains to filter $knownExtensionDomains = [ 'cdn.honey.io', 'honey.io', 'extensions.grammarly.com', 'grammarly.com', 'lastpass.com', 'dashlane.com', 'bitwarden.com', '1password.com' ]; $reports = []; if (isset($report[0]) && is_array($report[0])) { $reports = $report; } else { $reports = [$report]; } $shouldLog = false; foreach ($reports as $singleReport) { $blockedURL = $singleReport['body']['blockedURL'] ?? ''; $sourceFile = $singleReport['body']['sourceFile'] ?? ''; $isExtension = false; // Only ignore if it's DEFINITELY from a browser extension (protocol check) foreach ($extensionIndicators as $indicator) { if (stripos($sourceFile, $indicator) !== false) { $isExtension = true; break; } } // Or from a known safe extension domain if (!$isExtension) { foreach ($knownExtensionDomains as $domain) { if (stripos($blockedURL, $domain) !== false || stripos($sourceFile, $domain) !== false) { $isExtension = true; break; } } } if (!$isExtension) { $shouldLog = true; break; } } if ($shouldLog) { $log->save('csp-report', $data); } } http_response_code(204); exit; I am not sure if I am overly complicating things. It honestly seems pretty hard to get all this working when you're relying on different third party scripts. I see that even google.com has a very limited policy and it's in report-only mode, so maybe everyone has given up on this anyway 😁
-
Hi @ryan - I mentioned this here: https://github.com/processwire/processwire-issues/issues/2184#issuecomment-3901936805 but I want to make sure it isn't lost. I really think PW needs a $config->cspNonce that gets injected into all core scripts and can be used by all module developers as well. I am doing this in my config.php $config->cspNonce = base64_encode(random_bytes(16)); but if it was in /wire/config.php we could all use it - in modules and template files. Another possibility is maybe have a core method like: function getNonce(): ?string { return preg_match('#^Content-Security-Policy(?:-Report-Only)?:.*\s(?:script-src|script-src-elem)\s+(?:[^;]+\s)?\'nonce-([\w+/]+=*)\'#mi', implode("\n", headers_list()), $m) ? $m[1] : null; } that finds an existing nonce set for either script-src or script-src-elem and use the result of that to add to core scripts (src file and inline). But I don't think PW should set the CSP itself - I think that should be up to us. Please let me know if you have any thoughts or questions - I've been trying to up my game in the CSP area and this would make life a lot easier because at the moment I can't lock down the PW admin without it so I need separate CSP's for frontend and admin.
-
Hi @maximus - this is intriguing - I use Twilio a lot, but would love a free alternative. You mention that Telegram is "already installed on most devices" but I can't find any evidence online that this is the case. Can you please cite your source for this. Thanks.
-
timezone FieldtypeTimezone - Dynamic Timezone Field with DST Support
adrian replied to maximus's topic in Modules/Plugins
Thanks for this @maximus - timezone DST changes are definitely an annoyance in development. I am curious though why you have a limited set of timezones available, rather than the complete set? I feel like it might be particularly problematic if you rely on a service like https://ipapi.co/ to determine the user's timezone from their IP address. Many of the timezones returned from that are not likely to match your list. I can see that the way you have organized them makes it much simpler for user selection, and maybe that is your primary goal but I still wonder about how it might impact its functionality. -
What do you mean by "not as reliably"? Does it fail sometimes? I use this in so many places and I don't think I've ever had an issue so perhaps there is something else at play here.
-
<?php namespace ProcessWire; if($config->ajax) { bd('AJAX request detected'); echo "Replaced page content"; exit(); } ?> <script> function loadlist(segment='') { var xhttp = new XMLHttpRequest(); xhttp.onreadystatechange = function() { if (this.readyState == 4 && this.status == 200) { document.getElementById("main").innerHTML = this.responseText; } }; xhttp.open("GET", "<?=$page->url?>"+segment, true); xhttp.setRequestHeader("X-Requested-With", "XMLHttpRequest"); xhttp.send(); } </script> <div id="content"> <div id="main">Basic page content</div> <p><button onclick="loadlist('')">Load List</button></p> </div> Note the replacement of the text in "main" and that Tracy detects the AJAX request.