Skip to content
This is the documentation for pieceofcake2/security-dast, a CakePHP 2.x plugin for pieceofcake2/cakephp. About this fork.

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 ​

  1. Look at the response yourself, e.g. the headers and the cookies of a page:

    bash
    curl -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.

  2. Run the scan again: the alert is no longer in the report.

  3. Check production too, with a baseline scan of --url on staging or production: the web server, the proxy and PHP of production are not those of the scan.

Alert IDAlertRiskFix
40012, 40014, 40026Cross Site Scripting (Reflected, Persistent, DOM Based)HighXSS (full scan)
40018, 40019, 40020, 40021, 40022, 40024SQL InjectionHighSQL injection (full scan)
6, 7Path Traversal, Remote File InclusionHighPaths and files of the request (full scan)
90020Remote OS Command InjectionHighCommands (full scan)
20019External RedirectHighRedirects (full scan)
10038Content Security Policy (CSP) Header Not SetMediumSecurity headers
10020Missing Anti-clickjacking HeaderMediumSecurity headers
10202Absence of Anti-CSRF TokensMediumForms without a CSRF token
90003Sub Resource Integrity Attribute MissingMediumScripts of other sites
10021X-Content-Type-Options Header MissingLowSecurity headers
10063Permissions Policy Header Not SetLowSecurity headers
90004Cross-Origin-Embedder/Opener/Resource-Policy Header Missing or InvalidLowSecurity headers
10035Strict-Transport-Security Header Not SetLowSecurity headers
10010Cookie No HttpOnly FlagLowCookies
10011Cookie Without Secure FlagLowCookies
10054Cookie without SameSite AttributeLowCookies
10023Information Disclosure - Debug Error MessagesLowError pages
90022Application Error DisclosureLowError pages
10037Server Leaks Information via "X-Powered-By"LowVersions in the headers
10036Server Leaks Version Information via "Server"LowVersions in the headers
10017Cross-Domain JavaScript Source File InclusionLowScripts of other sites
10049Storable and Cacheable Content, Non-Storable ContentInformationalCache
10109, 10111, 10027, 10096Modern Web Application, Authentication Request Identified, Information Disclosure - Suspicious Comments, Timestamp DisclosureInformationalInformation

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:

php
<!-- 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():

php
// 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, as CakeErrorController extends AppController; the files of webroot/ do not.
php
// 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 of webroot/js/, or add the sites to the policy, e.g. script-src 'self' https://cdn.example.com. Introduce a policy step by step:
    1. Send it as Content-Security-Policy-Report-Only: it blocks nothing, and the console of the browser lists what it would block.
    2. 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.
    3. Send it as Content-Security-Policy once the console is clean.
  • 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-age seconds, 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.

php
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). For CookieComponent, 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). For CookieComponent, 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:

    php
    Configure::write('Session', [
        'defaults' => 'php',
        'ini' => ['session.cookie_samesite' => 'Lax'],
    ]);

    CookieComponent has no option for it: add it in the web server, with Header always edit Set-Cookie of Apache, or proxy_cookie_flags of 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.x comes from PHP: set expose_php = Off in php.ini of production.
  • Server: Apache/2.4.x (Ubuntu) comes from the web server: ServerTokens Prod (Apache) or server_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:

php
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=1 crawls 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.

See LICENSE for copyright and license information. Not affiliated with the Cake Software Foundation.