<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:media="http://search.yahoo.com/mrss/"><channel><title>Configuration | FirewallSync</title><link>https://firewallsync.com/tags/configuration/</link><description>Practical cybersecurity writing for engineering teams that ship fast.</description><language>en-us</language><image><url>https://firewallsync.com/icon-512.png</url><title>FirewallSync</title><link>https://firewallsync.com/</link></image><lastBuildDate>Tue, 27 Oct 2026 04:55:00 +0000</lastBuildDate><atom:link href="https://firewallsync.com/tags/configuration/index.xml" rel="self" type="application/rss+xml"/><item><title>php.ini Security Hardening: Production Settings That Matter</title><link>https://firewallsync.com/posts/php-ini-security-hardening-production-settings/</link><guid>https://firewallsync.com/posts/php-ini-security-hardening-production-settings/</guid><pubDate>Tue, 27 Oct 2026 04:55:00 +0000</pubDate><dc:creator>Craig Leslie</dc:creator><category>AppSec &amp; APIs</category><description>PHP's own php.ini-production template still leaves several security settings at permissive defaults, including strict session mode, cookie flags and the X-Powered-By header. This guide lists the settings worth changing, shows what each one does with tested examples, explains where to set them so applications can't undo them, and is honest about the two directives that are not security boundaries.</description><enclosure url="https://firewallsync.com/posts/php-ini-security-hardening-production-settings/cover_hu4086639176484663441.jpg" type="image/jpeg" length="0"/><media:content url="https://firewallsync.com/posts/php-ini-security-hardening-production-settings/cover_hu4086639176484663441.jpg" medium="image" type="image/jpeg" width="1200" height="630"/><content:encoded><![CDATA[<p><img src="https://firewallsync.com/posts/php-ini-security-hardening-production-settings/cover_hu4086639176484663441.jpg" alt="A vintage control panel labelled Radiation, with rows of analogue meters, trip and reset lamps, and rotary dials" width="1200" height="630"></p><p>To harden PHP for production, start from <code>php.ini-production</code>, then change six settings it leaves permissive: turn off <code>expose_php</code>, turn on <code>session.use_strict_mode</code>, set the session cookie&rsquo;s <code>HttpOnly</code>, <code>Secure</code> and <code>SameSite</code> flags, and turn off <code>allow_url_fopen</code> if your code never reads URLs as files. Apply them in <code>php.ini</code> or with <code>php_admin_value</code> in the PHP-FPM pool so applications cannot undo them, and do not treat <code>disable_functions</code> or <code>open_basedir</code> as a sandbox.</p>
<p>Everything below was checked against PHP 8.4.27&rsquo;s <code>php.ini-production</code> and the PHP manual, and every behaviour we describe was run on PHP 8.3 CLI (Ubuntu 24.04 package). Where a setting behaves differently under PHP-FPM or Apache, we say so.</p>
<h2 id="the-baseline">The baseline</h2>
<table>
  <thead>
      <tr>
          <th>Setting</th>
          <th>Recommended</th>
          <th>Built-in default</th>
          <th>php.ini-production</th>
          <th>Why</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>expose_php</code></td>
          <td><code>Off</code></td>
          <td>On</td>
          <td><strong>On</strong></td>
          <td>Removes the <code>X-Powered-By: PHP/x.y.z</code> header</td>
      </tr>
      <tr>
          <td><code>display_errors</code></td>
          <td><code>Off</code></td>
          <td>On</td>
          <td>Off</td>
          <td>Errors can leak paths, queries and credentials</td>
      </tr>
      <tr>
          <td><code>display_startup_errors</code></td>
          <td><code>Off</code></td>
          <td>On</td>
          <td>Off</td>
          <td>Same, for startup</td>
      </tr>
      <tr>
          <td><code>log_errors</code></td>
          <td><code>On</code></td>
          <td>Off</td>
          <td>On</td>
          <td>You still need the errors, in a log</td>
      </tr>
      <tr>
          <td><code>allow_url_include</code></td>
          <td><code>Off</code></td>
          <td>Off</td>
          <td>Off</td>
          <td>Stops <code>include 'http://...'</code>; deprecated since 7.4</td>
      </tr>
      <tr>
          <td><code>allow_url_fopen</code></td>
          <td><code>Off</code> if unused</td>
          <td>On</td>
          <td><strong>On</strong></td>
          <td>Limits URL reads via file functions</td>
      </tr>
      <tr>
          <td><code>enable_dl</code></td>
          <td><code>Off</code></td>
          <td>On</td>
          <td>Off</td>
          <td>Stops runtime extension loading</td>
      </tr>
      <tr>
          <td><code>session.use_strict_mode</code></td>
          <td><code>1</code></td>
          <td>0</td>
          <td><strong>0</strong></td>
          <td>Rejects attacker-chosen session IDs</td>
      </tr>
      <tr>
          <td><code>session.use_only_cookies</code></td>
          <td><code>1</code></td>
          <td>1</td>
          <td>1</td>
          <td>No session IDs in URLs</td>
      </tr>
      <tr>
          <td><code>session.cookie_httponly</code></td>
          <td><code>1</code></td>
          <td>0</td>
          <td><strong>unset</strong></td>
          <td>Keeps the session cookie away from JavaScript</td>
      </tr>
      <tr>
          <td><code>session.cookie_secure</code></td>
          <td><code>1</code></td>
          <td>0</td>
          <td><strong>unset</strong></td>
          <td>Session cookie only over HTTPS</td>
      </tr>
      <tr>
          <td><code>session.cookie_samesite</code></td>
          <td><code>Lax</code></td>
          <td>unset</td>
          <td><strong>unset</strong></td>
          <td>Cookie not sent on most cross-site requests</td>
      </tr>
      <tr>
          <td><code>session.use_trans_sid</code></td>
          <td><code>0</code></td>
          <td>0</td>
          <td>0</td>
          <td>No transparent session IDs in links</td>
      </tr>
  </tbody>
</table>
<p>&ldquo;Built-in default&rdquo; is what PHP uses with no <code>php.ini</code> at all (<code>php -n</code>). Bold marks the six settings where the production template differs from the hardened value. We measured both columns with the audit script at the end of this article.</p>
<h2 id="where-to-set-them-so-they-stick">Where to set them so they stick</h2>
<p>PHP settings come from several layers, and the later ones can override the earlier ones for directives that allow it: <code>php.ini</code> and its scan directory, web server or FPM pool configuration, <code>.user.ini</code> files in the document root, and <code>ini_set()</code> in code. Each directive has a changeable mode. <code>INI_SYSTEM</code> settings such as <code>disable_functions</code>, <code>allow_url_fopen</code> and <code>allow_url_include</code> can only be set in <code>php.ini</code> or server configuration.</p>
<p>For PHP-FPM, use <code>php_admin_value</code> and <code>php_admin_flag</code> for anything security-relevant. The PHP-FPM documentation says: &ldquo;Settings defined with php_admin_value and php_admin_flag cannot be overridden with ini_set.&rdquo; Plain <code>php_value</code> and <code>php_flag</code> can be.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ini" data-lang="ini"><span style="display:flex;"><span><span style="color:#75715e">; /etc/php/8.3/fpm/pool.d/app.conf (path varies by distribution)</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">[app]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">user</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">app</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">group</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">app</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">listen</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">/run/php/app.sock</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">php_admin_flag[expose_php]</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">off</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">php_admin_flag[display_errors]</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">off</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">php_admin_flag[log_errors]</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">on</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">php_admin_value[error_log]</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">/var/log/php/app-error.log</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">php_admin_flag[allow_url_fopen]</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">off</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">php_admin_value[session.use_strict_mode]</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">1</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">php_admin_value[session.cookie_httponly]</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">1</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">php_admin_value[session.cookie_secure]</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">1</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">php_admin_value[session.cookie_samesite]</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">Lax</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">; Only execute .php files (the default also allows .phar)</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">security.limit_extensions</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">.php</span>
</span></span></code></pre></div><p>This is illustrative: we validated the directive names and values against the PHP-FPM documentation and our CLI tests, but did not run this pool file. Restart the pool after editing. For Apache with mod_php, the equivalent is <code>php_admin_value</code> and <code>php_admin_flag</code> in the virtual host.</p>
<p>You can see runtime locking in action. With <code>disable_functions</code> set in configuration, <code>ini_set('disable_functions', '')</code> returns <code>false</code> and changes nothing. With <code>open_basedir</code> set, an attempt to widen it to <code>/</code> also returns <code>false</code>.</p>
<h2 id="stop-advertising-your-php-version">Stop advertising your PHP version</h2>
<p><code>php.ini-production</code> ships with <code>expose_php = On</code>. Its comment says the header &ldquo;is no security threat in any way&rdquo;, and that is fair: hiding the version does not fix anything. But the header tells every scanner which PHP release you run, which turns mass scanning for a new CVE into a simple header match. We ran PHP&rsquo;s built-in server with each value:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-console" data-lang="console"><span style="display:flex;"><span>$ curl -sI http://127.0.0.1:8099/index.php | grep -i x-powered   <span style="color:#75715e"># expose_php=1</span>
</span></span><span style="display:flex;"><span>X-Powered-By: PHP/8.3.6
</span></span><span style="display:flex;"><span>$ curl -sI http://127.0.0.1:8099/index.php | grep -i x-powered   <span style="color:#75715e"># expose_php=0</span>
</span></span><span style="display:flex;"><span>$
</span></span></code></pre></div><p>Frameworks and reverse proxies can add their own version headers, so check the response at the edge too. Our guide to <a href="/posts/http-security-headers-hsts-nosniff-referrer-policy-and-permissions-policy-explained/">HTTP security headers</a> covers the headers you do want to send.</p>
<h2 id="log-errors-never-display-them">Log errors, never display them</h2>
<p>The production template sets <code>display_errors = Off</code> and <code>log_errors = On</code>, and its comment explains why: displayed errors could leak &ldquo;sensitive information &hellip; such as database usernames and passwords or worse.&rdquo; The built-in defaults are the opposite, so a server that runs PHP without loading a <code>php.ini</code> (a misconfigured container is the usual case) will print errors to users. Point <code>error_log</code> at a file or <code>stderr</code> that your logging pipeline collects.</p>
<h2 id="turn-off-url-includes-and-if-you-can-url-file-reads">Turn off URL includes and, if you can, URL file reads</h2>
<p><code>allow_url_include</code> lets <code>include</code> and <code>require</code> load code from a URL. It is off by default and the PHP manual marks it &ldquo;Deprecated as of PHP 7.4.0&rdquo;; enabling it now prints a deprecation notice at startup. Keep it off.</p>
<p><code>allow_url_fopen</code> is broader: it lets <code>file_get_contents()</code>, <code>fopen()</code> and similar functions open <code>http://</code> and <code>ftp://</code> URLs. If your application only makes HTTP requests through cURL or an HTTP client library, turning it off removes one way for user input passed to a file function to become an outbound request. It does not stop server-side request forgery through your HTTP client; see our <a href="/posts/ssrf-explained-how-server-side-request-forgery-works-and-how-to-prevent-it/">SSRF guide</a> for that. Test before you change it: some libraries and package managers rely on it.</p>
<h2 id="session-settings">Session settings</h2>
<p>PHP&rsquo;s session defaults favour compatibility. Four changes matter.</p>
<h3 id="strict-mode">Strict mode</h3>
<p>The PHP manual is blunt: &ldquo;Enabling session.use_strict_mode is mandatory for general session security. All sites are advised to enable this.&rdquo; Without it, PHP accepts any session ID the browser sends, even one the server never issued, which enables session fixation. We simulated a browser sending an attacker-chosen <code>PHPSESSID</code> cookie:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#f92672">&lt;?</span><span style="color:#a6e22e">php</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// strict.php: run with `php -d session.use_strict_mode=0|1 strict.php`
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#a6e22e">ini_set</span>(<span style="color:#e6db74">&#39;session.save_path&#39;</span>, <span style="color:#66d9ef">__DIR__</span>);
</span></span><span style="display:flex;"><span>$_COOKIE[<span style="color:#a6e22e">session_name</span>()] <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;attackerchosenid123456&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">session_start</span>();
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">printf</span>(<span style="color:#e6db74">&#34;use_strict_mode=%s -&gt; session_id=%s</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>, <span style="color:#a6e22e">ini_get</span>(<span style="color:#e6db74">&#39;session.use_strict_mode&#39;</span>), <span style="color:#a6e22e">session_id</span>());
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-console" data-lang="console"><span style="display:flex;"><span>$ php -d session.use_strict_mode<span style="color:#f92672">=</span><span style="color:#ae81ff">0</span> strict.php
</span></span><span style="display:flex;"><span>use_strict_mode=0 -&gt; session_id=attackerchosenid123456
</span></span><span style="display:flex;"><span>$ php -d session.use_strict_mode<span style="color:#f92672">=</span><span style="color:#ae81ff">1</span> strict.php
</span></span><span style="display:flex;"><span>use_strict_mode=1 -&gt; session_id=3e4393b3059589b68078c6a9f5c4fe34
</span></span></code></pre></div><p>Strict mode does not replace calling <code>session_regenerate_id(true)</code> after login; do both.</p>
<h3 id="cookie-flags">Cookie flags</h3>
<ul>
<li><code>session.cookie_httponly = 1</code> makes the cookie inaccessible to JavaScript. The manual says it &ldquo;can effectively help to reduce identity theft through XSS attacks&rdquo;. It does not stop an XSS payload from making requests as the user.</li>
<li><code>session.cookie_secure = 1</code> sends the cookie only over HTTPS. The manual notes that with it on, &ldquo;sessions only work with HTTPS connections&rdquo;. If TLS terminates at a load balancer, that means the browser&rsquo;s connection: the flag is a cookie attribute the browser enforces, so it works as long as users reach you over HTTPS.</li>
<li><code>session.cookie_samesite = Lax</code> stops the browser sending the session cookie on most cross-site subrequests and form posts. It reduces CSRF exposure but does not remove the need for tokens; we compare the options in <a href="/posts/csrf-protection-samesite-cookies-tokens-and-fetch-metadata-compared/">CSRF protection: SameSite, tokens and Fetch Metadata</a>.</li>
</ul>
<p>These cookie protections address token theft through the browser. They do nothing about a cookie stolen from the endpoint itself, which our explainer on <a href="/posts/what-is-token-theft-and-why-mfa-doesnt-always-stop-it/">token theft and MFA</a> covers.</p>
<h2 id="request-size-and-resource-limits">Request size and resource limits</h2>
<p><code>post_max_size</code> (8M in both defaults), <code>upload_max_filesize</code> (2M), <code>max_input_vars</code> (1000), <code>memory_limit</code> (128M) and <code>max_execution_time</code> (30 seconds) are mainly availability controls. Set them to what your application needs, not higher, and enforce body size limits at the web server or proxy as well, because those apply before PHP parses anything.</p>
<p>These limits briefly became security-critical in October 2026. PHP&rsquo;s stopgap advice for <a href="/posts/php2shell-cve-2026-109618-cve-2026-109619-patch-and-hunt/">PHP2Shell (CVE-2026-109618 and CVE-2026-109619)</a> included a low <code>post_max_size</code>, <code>memory_limit=-1</code> set non-overridably and <code>session.upload_progress.enabled=0</code>. Those were emergency measures for unpatched servers and PHP said they &ldquo;do not make an installation safe&rdquo;. Once patched, keep a finite <code>memory_limit</code> unless you have a container or cgroup limit doing that job. Note that <code>session.upload_progress.enabled</code> is on by default and in the production template; if your application does not show upload progress bars, turning it off removes a feature you do not use.</p>
<h2 id="what-not-to-rely-on">What not to rely on</h2>
<p>Two directives look like sandboxes and are not.</p>
<p><strong><code>disable_functions</code></strong> removes named internal functions. With <code>system</code>, <code>exec</code> and <code>shell_exec</code> disabled, calling <code>system()</code> raises <code>Error: Call to undefined function system()</code>, which defeats the laziest webshells. But the PHP manual says plainly: &ldquo;This directive can be circumvented and should not be considered a sufficient security measure for shared hosting environments.&rdquo; It only covers internal functions you thought to list, and in PHP 8 a disabled function&rsquo;s name can be redefined by userland code.</p>
<p><strong><code>open_basedir</code></strong> limits which paths PHP&rsquo;s file functions can open, and the manual says symlinks are resolved so they cannot be used to escape it. It is still not an isolation boundary. The same manual notes that &ldquo;With dynamic loading, it&rsquo;s possible to ignore all open_basedir restrictions&rdquo;, which is one reason to keep <code>enable_dl</code> off, and the restriction only applies to PHP&rsquo;s own file access, not to anything a spawned process does. It also disables PHP&rsquo;s realpath cache, which has a performance cost.</p>
<p>If you host more than one application, or code you do not fully trust, isolate them at the operating system level: a separate PHP-FPM pool per application running as its own Unix user, with file permissions that stop one user reading another&rsquo;s files, or separate containers. That is the boundary; the two directives above are defence in depth inside it.</p>
<h2 id="audit-what-is-actually-running">Audit what is actually running</h2>
<p>The settings that matter are the ones the serving process loaded, which may not be the <code>php.ini</code> you edited. The CLI and FPM usually read different files (on Debian and Ubuntu, <code>/etc/php/&lt;version&gt;/cli/</code> and <code>/etc/php/&lt;version&gt;/fpm/</code>). This script prints the effective values against the baseline above. Run it under the SAPI you care about: from the command line for the CLI, or temporarily as a web request for FPM or mod_php (then delete it, since it reveals configuration).</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#f92672">&lt;?</span><span style="color:#a6e22e">php</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// ini-audit.php: compare effective settings with a hardened baseline.
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>$baseline <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;expose_php&#39;</span>               <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;0&#39;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;display_errors&#39;</span>           <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;0&#39;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;display_startup_errors&#39;</span>   <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;0&#39;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;log_errors&#39;</span>               <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;1&#39;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;allow_url_include&#39;</span>        <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;0&#39;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;allow_url_fopen&#39;</span>          <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;0&#39;</span>,   <span style="color:#75715e">// only if the app never opens URLs with file functions
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#e6db74">&#39;enable_dl&#39;</span>                <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;0&#39;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;session.use_strict_mode&#39;</span>  <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;1&#39;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;session.use_only_cookies&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;1&#39;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;session.cookie_httponly&#39;</span>  <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;1&#39;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;session.cookie_secure&#39;</span>    <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;1&#39;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;session.cookie_samesite&#39;</span>  <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;Lax&#39;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;session.use_trans_sid&#39;</span>    <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;0&#39;</span>,
</span></span><span style="display:flex;"><span>];
</span></span><span style="display:flex;"><span>$norm <span style="color:#f92672">=</span> <span style="color:#a6e22e">fn</span>($v) <span style="color:#f92672">=&gt;</span> <span style="color:#a6e22e">match</span> (<span style="color:#a6e22e">strtolower</span>((<span style="color:#a6e22e">string</span>)$v)) {
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#39;on&#39;</span>, <span style="color:#e6db74">&#39;true&#39;</span>, <span style="color:#e6db74">&#39;yes&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;1&#39;</span>, <span style="color:#e6db74">&#39;off&#39;</span>, <span style="color:#e6db74">&#39;false&#39;</span>, <span style="color:#e6db74">&#39;no&#39;</span>, <span style="color:#e6db74">&#39;&#39;</span> <span style="color:#f92672">=&gt;</span> <span style="color:#e6db74">&#39;0&#39;</span>, <span style="color:#66d9ef">default</span> <span style="color:#f92672">=&gt;</span> (<span style="color:#a6e22e">string</span>)$v };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>$fail <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">foreach</span> ($baseline <span style="color:#66d9ef">as</span> $key <span style="color:#f92672">=&gt;</span> $want) {
</span></span><span style="display:flex;"><span>    $have <span style="color:#f92672">=</span> <span style="color:#a6e22e">ini_get</span>($key);
</span></span><span style="display:flex;"><span>    $ok <span style="color:#f92672">=</span> ($key <span style="color:#f92672">===</span> <span style="color:#e6db74">&#39;session.cookie_samesite&#39;</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">?</span> <span style="color:#a6e22e">in_array</span>($have, [<span style="color:#e6db74">&#39;Lax&#39;</span>, <span style="color:#e6db74">&#39;Strict&#39;</span>], <span style="color:#66d9ef">true</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">:</span> $norm($have) <span style="color:#f92672">===</span> $want;
</span></span><span style="display:flex;"><span>    $fail <span style="color:#f92672">+=</span> $ok <span style="color:#f92672">?</span> <span style="color:#ae81ff">0</span> <span style="color:#f92672">:</span> <span style="color:#ae81ff">1</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">printf</span>(<span style="color:#e6db74">&#34;%-26s %-6s want %-5s %s</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>, $key, <span style="color:#a6e22e">var_export</span>($have, <span style="color:#66d9ef">true</span>), $want, $ok <span style="color:#f92672">?</span> <span style="color:#e6db74">&#39;ok&#39;</span> <span style="color:#f92672">:</span> <span style="color:#e6db74">&#39;CHECK&#39;</span>);
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">printf</span>(<span style="color:#e6db74">&#34;</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">%d setting(s) differ from the baseline (SAPI: %s, ini: %s)</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">&#34;</span>,
</span></span><span style="display:flex;"><span>    $fail, <span style="color:#a6e22e">PHP_SAPI</span>, <span style="color:#a6e22e">php_ini_loaded_file</span>() <span style="color:#f92672">?:</span> <span style="color:#e6db74">&#39;none&#39;</span>);
</span></span></code></pre></div><p>Run against PHP 8.4.27&rsquo;s <code>php.ini-production</code> with <code>php -c php.ini-production ini-audit.php</code> on PHP 8.3 CLI, it reported:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-console" data-lang="console"><span style="display:flex;"><span>expose_php                 &#39;1&#39;    want 0     CHECK
</span></span><span style="display:flex;"><span>display_errors             &#39;&#39;     want 0     ok
</span></span><span style="display:flex;"><span>display_startup_errors     &#39;&#39;     want 0     ok
</span></span><span style="display:flex;"><span>log_errors                 &#39;1&#39;    want 1     ok
</span></span><span style="display:flex;"><span>allow_url_include          &#39;&#39;     want 0     ok
</span></span><span style="display:flex;"><span>allow_url_fopen            &#39;1&#39;    want 0     CHECK
</span></span><span style="display:flex;"><span>enable_dl                  &#39;&#39;     want 0     ok
</span></span><span style="display:flex;"><span>session.use_strict_mode    &#39;0&#39;    want 1     CHECK
</span></span><span style="display:flex;"><span>session.use_only_cookies   &#39;1&#39;    want 1     ok
</span></span><span style="display:flex;"><span>session.cookie_httponly    &#39;&#39;     want 1     CHECK
</span></span><span style="display:flex;"><span>session.cookie_secure      &#39;0&#39;    want 1     CHECK
</span></span><span style="display:flex;"><span>session.cookie_samesite    &#39;&#39;     want Lax   CHECK
</span></span><span style="display:flex;"><span>session.use_trans_sid      &#39;0&#39;    want 0     ok
</span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010"></span>6 setting(s) differ from the baseline (SAPI: cli, ini: php.ini-production)
</span></span></code></pre></div><p>With no <code>php.ini</code> at all (<code>php -n</code>), 10 of the 13 differed. With the hardened values applied, none did.</p>
<h2 id="limits-of-this-article">Limits of this article</h2>
<p>This covers PHP&rsquo;s own configuration, not your web server, framework or application code. We tested behaviour on PHP 8.3 CLI and read defaults from PHP 8.4.27&rsquo;s template; check the manual for your version, since defaults and deprecations change between releases. The PHP-FPM pool example was not run. The baseline is a starting point for a typical web application: turning off <code>allow_url_fopen</code> in particular can break code that depends on it, so test in staging first.</p>
<h2 id="sources-and-verification">Sources and verification</h2>
<table>
  <thead>
      <tr>
          <th>Important claim</th>
          <th>Source</th>
          <th>Status</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>php.ini-production values (expose_php On, use_strict_mode 0, cookie flags unset, display_errors Off, log_errors On) and comments quoted</td>
          <td><a href="https://github.com/php/php-src/blob/php-8.4.27/php.ini-production">php.ini-production at php-8.4.27</a></td>
          <td>Verified (and measured with the audit script)</td>
      </tr>
      <tr>
          <td>Built-in defaults</td>
          <td><code>php -n</code> on PHP 8.3.6 (Ubuntu 24.04 package), October 27, 2026</td>
          <td>Verified (tested)</td>
      </tr>
      <tr>
          <td>use_strict_mode is &ldquo;mandatory for general session security&rdquo;; cookie_httponly and cookie_secure behaviour</td>
          <td><a href="https://www.php.net/manual/en/session.configuration.php">PHP manual: session runtime configuration</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>disable_functions &ldquo;can be circumvented&rdquo;; PHP 8 redefinition; open_basedir symlink and dynamic-loading notes; realpath cache disabled</td>
          <td><a href="https://www.php.net/manual/en/ini.core.php">PHP manual: core php.ini directives</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>allow_url_include deprecated as of 7.4.0</td>
          <td><a href="https://www.php.net/manual/en/filesystem.configuration.php">PHP manual: filesystem runtime configuration</a></td>
          <td>Verified (and tested: deprecation notice)</td>
      </tr>
      <tr>
          <td>php_admin_value cannot be overridden with ini_set; security.limit_extensions default .php .phar</td>
          <td><a href="https://www.php.net/manual/en/install.fpm.configuration.php">PHP-FPM configuration</a></td>
          <td>Verified</td>
      </tr>
      <tr>
          <td>Strict mode rejects an attacker-chosen ID; disable_functions error; X-Powered-By header; ini_set refusing to loosen settings</td>
          <td>Our tests on PHP 8.3 CLI and built-in server</td>
          <td>Verified (tested)</td>
      </tr>
      <tr>
          <td>PHP2Shell stopgaps and &ldquo;do not make an installation safe&rdquo;</td>
          <td><a href="https://www.php.net/archive/2026.php">PHP security advisory, October 21, 2026</a></td>
          <td>Verified</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item></channel></rss>