Alerts
The alerts that ZAP raises most often on a CakePHP 2 application, by the baseline and by the full scan, what they mean, and how to fix them. Each page of the report also has the description and the solution of ZAP, and a link to the page of the alert on zaproxy.org.
Most alerts are fixed once for the whole application: the security headers in AppController and the web server, the cookies in config/core.php, and the error pages with the debug level. The alerts of the full scan (XSS, SQL injection, ...) are fixed in the code of the page: the same places are found by the rules of pieceofcake2/security-sast, which give more patterns of the fix.
Checking a Fix
Look at the response yourself, e.g. the headers and the cookies of a page:
bashcurl -sI https://staging.example.com/ | grep -iE 'content-security|x-frame|x-content-type|set-cookie|x-powered'The developer tools of the browser (Network, then the headers of a request, and Application, then the cookies) show the same.
Run the scan again: the alert is no longer in the report.
Check production too, with a baseline scan of
--urlon staging or production: the web server, the proxy and PHP of production are not those of the scan.
| Alert ID | Alert | Risk | Fix |
|---|---|---|---|
| 40012, 40014, 40026 | Cross Site Scripting (Reflected, Persistent, DOM Based) | High | XSS (full scan) |
| 40018, 40019, 40020, 40021, 40022, 40024 | SQL Injection | High | SQL injection (full scan) |
| 6, 7 | Path Traversal, Remote File Inclusion | High | Paths and files of the request (full scan) |
| 90020 | Remote OS Command Injection | High | Commands (full scan) |
| 20019 | External Redirect | High | Redirects (full scan) |
| 10038 | Content Security Policy (CSP) Header Not Set | Medium | Security headers |
| 10020 | Missing Anti-clickjacking Header | Medium | Security headers |
| 10202 | Absence of Anti-CSRF Tokens | Medium | Forms without a CSRF token |
| 90003 | Sub Resource Integrity Attribute Missing | Medium | Scripts of other sites |
| 10021 | X-Content-Type-Options Header Missing | Low | Security headers |
| 10063 | Permissions Policy Header Not Set | Low | Security headers |
| 90004 | Cross-Origin-Embedder/Opener/Resource-Policy Header Missing or Invalid | Low | Security headers |
| 10035 | Strict-Transport-Security Header Not Set | Low | Security headers |
| 10010 | Cookie No HttpOnly Flag | Low | Cookies |
| 10011 | Cookie Without Secure Flag | Low | Cookies |
| 10054 | Cookie without SameSite Attribute | Low | Cookies |
| 10023 | Information Disclosure - Debug Error Messages | Low | Error pages |
| 90022 | Application Error Disclosure | Low | Error pages |
| 10037 | Server Leaks Information via "X-Powered-By" | Low | Versions in the headers |
| 10036 | Server Leaks Version Information via "Server" | Low | Versions in the headers |
| 10017 | Cross-Domain JavaScript Source File Inclusion | Low | Scripts of other sites |
| 10049 | Storable and Cacheable Content, Non-Storable Content | Informational | Cache |
| 10109, 10111, 10027, 10096 | Modern Web Application, Authentication Request Identified, Information Disclosure - Suspicious Comments, Timestamp Disclosure | Informational | Information |
XSS
A value of the request (a parameter, a field of a form) is printed in a page without escaping: a script in it runs in the browser of the user who opens the page, with their session. Reflected: in the response of the request; persistent: saved, then printed in another page; DOM based: by JavaScript of the page.
Escape every value in the views with h(), and build the forms and links with the helpers, which escape:
<!-- Found -->
<p>Sent: <?php echo $message; ?></p>
<!-- Fixed -->
<p>Sent: <?= h($message) ?></p>The rule cakephp2-view-unescaped-output of pieceofcake2/security-sast finds these places in the code. For the DOM based XSS, do not put values in the page with innerHTML or document.write() in JavaScript: use textContent. A Content Security Policy without 'unsafe-inline' (Security headers) also blocks most of these scripts.
SQL Injection
A value of the request goes into SQL as it is: the attacker can read or change the database. Use the conditions arrays of find(), which escape the values, or the values of query():
// Found
$this->Post->query("SELECT * FROM posts WHERE title LIKE '%" . $q . "%'");
// Fixed
$this->Post->find('all', ['conditions' => ['Post.title LIKE' => '%' . $q . '%']]);
$this->Post->query('SELECT * FROM posts WHERE title LIKE ?', ['%' . $q . '%']);pieceofcake2/security-sast finds these places with the rules cakephp2-query-concatenation and cakephp2-conditions-string.
Paths and Files of the Request
A value of the request names a file to read or include, or a view to render: ../ in it reaches other files. Map the value to a list of allowed names instead of using it as a path, and check uploaded file names with basename(). See the rule cakephp2-render-request.
Commands
A value of the request goes into a command of the system (exec(), shell_exec(), system(), backticks): the attacker runs commands on the server. Avoid the command, or pass each value with escapeshellarg().
Redirects
A value of the request is the URL of a redirect: a link of the site can send its users to another site, e.g. a copy of the login page. Redirect only to paths of the application, e.g. with a route array, or check the URL against a list. See the rule cakephp2-redirect-request.
Security Headers
The headers that tell the browser to limit what a page can do: which scripts it may run (CSP), whether another site may frame it (clickjacking), whether it may guess the type of a file, and which features and cross-origin resources it may use.
Set them in one place:
- In the web server (recommended): every response gets them, also the files of
webroot/and the errors of the web server. See Web Server Settings for complete settings of nginx and Apache. - In
AppController, when you cannot change the web server: every page of CakePHP gets them, the error pages (404, 500) too, asCakeErrorControllerextendsAppController; the files ofwebroot/do not.
// src/Controller/AppController.php (app/Controller/AppController.php)
public function beforeFilter()
{
parent::beforeFilter();
$this->response->header([
'Content-Security-Policy' => "default-src 'self'; frame-ancestors 'self'; form-action 'self'",
'X-Frame-Options' => 'SAMEORIGIN',
'X-Content-Type-Options' => 'nosniff',
'Referrer-Policy' => 'strict-origin-when-cross-origin',
'Permissions-Policy' => 'camera=(), microphone=(), geolocation=()',
'Cross-Origin-Opener-Policy' => 'same-origin',
'Cross-Origin-Resource-Policy' => 'same-origin',
]);
if ($this->request->is('ssl')) {
$this->response->header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
}
}Not in both: a header sent twice, e.g. two X-Frame-Options, is invalid for some browsers.
- CSP:
default-src 'self'allows the scripts, styles and images of the application only. A page with inline scripts (<script>...</script>,$this->Html->scriptBlock(),onclick="...") or files of other sites needs more: move the scripts to files ofwebroot/js/, or add the sites to the policy, e.g.script-src 'self' https://cdn.example.com. Introduce a policy step by step:- Send it as
Content-Security-Policy-Report-Only: it blocks nothing, and the console of the browser lists what it would block. - Go through the pages (the End-to-End tests do), and move the inline scripts and styles to files, or add the sites that the pages need.
- Send it as
Content-Security-Policyonce the console is clean.
- Send it as
- Cross-Origin-Embedder-Policy (
require-corp) blocks the images and scripts of other sites that do not allow it. Set it only when the pages use no such resources; the alert of it is low. - Strict-Transport-Security only on HTTPS: a browser keeps it for
max-ageseconds, so make sure every page works on HTTPS first.
Forms without a CSRF Token
A form that changes data (method="post") without a token can be sent by another site in the name of a logged-in user (CSRF). Load SecurityComponent in AppController, and build the forms with FormHelper: $this->Form->create() and $this->Form->end() add the token (data[_Token][key]), and ZAP knows this name.
public $components = ['Security', 'Session'];ZAP reports the forms that are written in HTML (<form method="post">), or built without FormHelper. Build them with FormHelper; for an action that takes requests of other sites, e.g. a webhook, use $this->Security->unlockedActions for that action only. See also the rule cakephp2-security-csrf-disabled of pieceofcake2/security-sast.
Cookies
HttpOnly: a cookie without it can be read by JavaScript, and stolen by an XSS. The session cookie has it by default (
session.cookie_httponly). ForCookieComponent, set$this->Cookie->httpOnly = true;.Secure: a cookie without it is also sent over HTTP. The session cookie has it on HTTPS by default (
session.cookie_secure). ForCookieComponent, set$this->Cookie->secure = true;on HTTPS.SameSite: limits the cookies that are sent with the requests of other sites. For the session, set it in
config/core.php:phpConfigure::write('Session', [ 'defaults' => 'php', 'ini' => ['session.cookie_samesite' => 'Lax'], ]);CookieComponenthas no option for it: add it in the web server, withHeader always edit Set-Cookieof Apache, orproxy_cookie_flagsof nginx in front of another server (Web Server Settings).
Do not set session.cookie_httponly or session.cookie_secure to false; pieceofcake2/security-sast checks this in the configuration (cakephp2-config-session-cookie). Check the cookies with curl -sI or in the developer tools: each Set-Cookie of the session shows HttpOnly, Secure (on HTTPS) and SameSite=Lax.
Error Pages
An error page with a stack trace, the SQL or the paths of the files tells an attacker how the application works. In production, debug must be 0 (APP_DEBUG unset or 0): CakePHP then shows the pages of templates/Errors/ (app/View/Errors/) without the details.
The built-in web server runs with APP_ENV=test, where debug is usually 0 too. "Application Error Disclosure" on many pages of the scan means that they failed with an error 500: check the log (logs/error.log), e.g. the test database has no tables.
Versions in the Headers
X-Powered-By: PHP/8.xcomes from PHP: setexpose_php = Offinphp.iniof production.Server: Apache/2.4.x (Ubuntu)comes from the web server:ServerTokens Prod(Apache) orserver_tokens off;(nginx). With PHP-FPM behind nginx,fastcgi_hide_header X-Powered-By;hides the version of PHP too (Web Server Settings).
The built-in web server of the scan sends X-Powered-By with the php.ini of the machine of CI, so this alert may show there and not in production: check the headers of production, or ignore it in the scan with --ignore=10037 when production is set.
Scripts of Other Sites
A script of another site (a CDN) runs with the rights of the page: if that site is attacked, so is the application. Serve the files from webroot/js/, or give their hash with integrity (Subresource Integrity), which the browser checks:
echo $this->Html->script('https://cdn.example.com/lib.min.js', [
'integrity' => 'sha384-...',
'crossorigin' => 'anonymous',
]);The hash is on the page of the file of the CDN, or openssl dgst -sha384 -binary lib.min.js | openssl base64 -A.
Cache
"Storable and Cacheable Content" is informational: a page may be kept by a proxy. For the pages of a logged-in user, send Cache-Control: no-store with $this->response->disableCache() in the controller, e.g. in beforeFilter() when the user is logged in.
Information
These alerts are informational and need no fix in most applications:
- Modern Web Application: the pages use JavaScript;
--ajax-spider=1crawls their links. - Authentication Request Identified: ZAP found a login form.
- Suspicious Comments: a comment in HTML or JavaScript has a word such as "TODO", "password" or "admin"; check that it does not tell anything that should be secret.
- Timestamp Disclosure: a number in a response looks like a timestamp; usually harmless.
