316 lines
9.5 KiB
PHP
316 lines
9.5 KiB
PHP
<?php
|
|
/**
|
|
* Input Security Guard - content only
|
|
* app/Views/docs/input-security.php
|
|
*
|
|
* Based on app/Filters/SecurityInputFilter.php
|
|
*/
|
|
?>
|
|
|
|
<p>
|
|
<code>SecurityInputFilter</code> is the request-level input guard for
|
|
high-confidence XSS detection. It inspects GET and POST values before
|
|
controller logic runs, canonicalizes user input to reduce encoding bypasses,
|
|
and blocks the request when a known dangerous pattern is detected.
|
|
</p>
|
|
|
|
<div class="callout info">
|
|
<span>i</span>
|
|
<div>
|
|
<strong>Good name in Features</strong>
|
|
This docs page is listed as <code>Input Security Guard</code> because the
|
|
filter is not only about sanitizing forms. It is a request gate that checks
|
|
user-controlled input before normal application logic continues.
|
|
</div>
|
|
</div>
|
|
|
|
<h2 id="overview">Overview</h2>
|
|
|
|
<div class="mermaid-wrapper">
|
|
<div class="mermaid">
|
|
flowchart TD
|
|
A[Incoming request] --> B[Read GET and POST input]
|
|
B --> C{Any input present}
|
|
C -->|No| D[Allow request]
|
|
C -->|Yes| E[Canonicalize each value]
|
|
E --> F[Trim input]
|
|
F --> G[Check XSS patterns]
|
|
G --> H{Dangerous pattern found}
|
|
H -->|No| D
|
|
H -->|Yes| I[Log metadata and hash]
|
|
I --> J[Return 403 JSON]
|
|
</div>
|
|
</div>
|
|
|
|
<h2 id="where-it-runs">Where it runs</h2>
|
|
|
|
<p>
|
|
The filter is globally registered in <code>app/Config/Filters.php</code> in
|
|
the <code>before</code> chain, which means it runs for normal incoming web
|
|
requests unless the route is explicitly excluded.
|
|
</p>
|
|
|
|
<pre><code class="language-php">'SecurityInputFilter' => SecurityInputFilter::class,
|
|
|
|
'before' => [
|
|
'SecurityInputFilter' => [
|
|
'except' => [
|
|
'/client/notification/create',
|
|
'/ticket/crud_mail_template/*',
|
|
'test_mail',
|
|
'leads/sendMail',
|
|
'ticket/reply'
|
|
]
|
|
],
|
|
]</code></pre>
|
|
|
|
<p>
|
|
The filter only reads:
|
|
</p>
|
|
|
|
<ul>
|
|
<li><code>$request->getGet()</code></li>
|
|
<li><code>$request->getPost()</code></li>
|
|
</ul>
|
|
|
|
<p>
|
|
It does not inspect uploaded file contents. File uploads are handled by the
|
|
separate <code>GlobalPostFileUploadGuard</code> filter.
|
|
</p>
|
|
|
|
<h2 id="what-it-checks">What it checks</h2>
|
|
|
|
<p>
|
|
The filter uses a focused list of high-confidence XSS patterns to reduce false
|
|
positives while still blocking obvious injection attempts.
|
|
</p>
|
|
|
|
<table>
|
|
<thead>
|
|
<tr><th>Category</th><th>Examples from the filter</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td>Script tags</td>
|
|
<td><code><script</code>, <code></script></code></td>
|
|
</tr>
|
|
<tr>
|
|
<td>JavaScript execution schemes</td>
|
|
<td><code>javascript:</code>, <code>vbscript:</code>, <code>data:text/html</code></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Inline event handlers</td>
|
|
<td><code>onclick=</code>, <code>onerror=</code>, <code>onload=</code></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Dangerous HTML tags</td>
|
|
<td><code><iframe</code>, <code><object</code>, <code><embed</code>, <code><applet</code>, <code><img</code></td>
|
|
</tr>
|
|
<tr>
|
|
<td>SVG / MathML vectors</td>
|
|
<td><code><svg</code>, <code><math</code></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Meta refresh payloads</td>
|
|
<td><code><meta http-equiv="refresh"</code></td>
|
|
</tr>
|
|
<tr>
|
|
<td>Injected src/href handlers</td>
|
|
<td>HTML tags using <code>src=javascript:</code> or <code>href=data:</code></td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<div class="callout warning">
|
|
<span>!</span>
|
|
<div>
|
|
<strong>Detection, not rich sanitization</strong>
|
|
This filter is a blocker for clearly malicious input. It is not a full HTML
|
|
sanitizer for rich text fields. If a feature needs controlled HTML input,
|
|
design that path explicitly and make sure the route is handled appropriately.
|
|
</div>
|
|
</div>
|
|
|
|
<h2 id="canonicalization">Canonicalization</h2>
|
|
|
|
<p>
|
|
Before pattern matching, the filter canonicalizes each value:
|
|
</p>
|
|
|
|
<ol class="steps">
|
|
<li>
|
|
<strong>URL decode</strong>
|
|
<p>Helps catch encoded payloads that would otherwise bypass naive matching.</p>
|
|
</li>
|
|
<li>
|
|
<strong>HTML entity decode</strong>
|
|
<p>Turns entity-encoded payloads into their real characters before detection.</p>
|
|
</li>
|
|
<li>
|
|
<strong>Strip invisible control characters</strong>
|
|
<p>Removes null bytes and other control characters from the evaluation string.</p>
|
|
</li>
|
|
<li>
|
|
<strong>Trim the final value</strong>
|
|
<p>Reduces noise before regex evaluation.</p>
|
|
</li>
|
|
</ol>
|
|
|
|
<pre><code class="language-php">private function canonicalize(string $value): string
|
|
{
|
|
$value = urldecode($value);
|
|
$value = html_entity_decode($value, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
|
|
|
return preg_replace('/[\x00-\x1F\x7F]/u', '', $value);
|
|
}</code></pre>
|
|
|
|
<p>
|
|
Array inputs are converted to JSON first, then canonicalized as a string.
|
|
</p>
|
|
|
|
<h2 id="block-behavior">Block behavior</h2>
|
|
|
|
<p>
|
|
When a pattern matches, the filter logs security metadata and immediately
|
|
returns a JSON <code>403</code> response:
|
|
</p>
|
|
|
|
<table>
|
|
<thead>
|
|
<tr><th>Logged field</th><th>Purpose</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr><td><code>ip</code></td><td>Source IP address</td></tr>
|
|
<tr><td><code>method</code></td><td>Request method</td></tr>
|
|
<tr><td><code>uri</code></td><td>Current request URL</td></tr>
|
|
<tr><td><code>field</code></td><td>Input field name</td></tr>
|
|
<tr><td><code>attack</code></td><td>Static marker <code>XSS_PATTERN</code></td></tr>
|
|
<tr><td><code>length</code></td><td>Canonicalized payload length</td></tr>
|
|
<tr><td><code>hash</code></td><td>SHA-256 hash of the canonicalized payload</td></tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<p>
|
|
The raw input value is not logged directly. The filter logs intent metadata and
|
|
a hash instead.
|
|
</p>
|
|
|
|
<pre><code class="language-json">{
|
|
"status": 403,
|
|
"error": "Forbidden",
|
|
"message": "Malicious input detected"
|
|
}</code></pre>
|
|
|
|
<h2 id="filter-exceptions">Filter exceptions</h2>
|
|
|
|
<p>
|
|
Some routes are explicitly excluded from the global security-input filter:
|
|
</p>
|
|
|
|
<table>
|
|
<thead>
|
|
<tr><th>Excluded route</th><th>Why developers should care</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td><code>/client/notification/create</code></td>
|
|
<td>Global input blocking does not run here.</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>/ticket/crud_mail_template/*</code></td>
|
|
<td>Template-editing paths often need richer content and should be handled deliberately.</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>test_mail</code></td>
|
|
<td>Bypassed globally.</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>leads/sendMail</code></td>
|
|
<td>Bypassed globally.</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>ticket/reply</code></td>
|
|
<td>Bypassed globally.</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<div class="callout danger">
|
|
<span>!</span>
|
|
<div>
|
|
<strong>Important developer rule</strong>
|
|
If you add a route to the exception list, you are taking responsibility for
|
|
validating and safely handling that input somewhere else in the request flow.
|
|
</div>
|
|
</div>
|
|
|
|
<h2 id="developer-steps">Developer steps</h2>
|
|
|
|
<p>
|
|
When building a new form, endpoint, or feature that accepts user input, follow
|
|
this checklist:
|
|
</p>
|
|
|
|
<ol class="steps">
|
|
<li>
|
|
<strong>Assume GET and POST are inspected automatically</strong>
|
|
<p>If your route is not in the exception list, the filter already evaluates GET and POST fields before controller code runs.</p>
|
|
</li>
|
|
<li>
|
|
<strong>Do not rely on this filter as your only validation</strong>
|
|
<p>Business validation, field-level validation, and output escaping are still required.</p>
|
|
</li>
|
|
<li>
|
|
<strong>Be careful with HTML-capable inputs</strong>
|
|
<p>If a feature legitimately accepts formatted HTML, do not silently fight the filter. Design a safe path for that route and document why it needs special handling.</p>
|
|
</li>
|
|
<li>
|
|
<strong>Only add filter exceptions deliberately</strong>
|
|
<p>If you exclude a route in <code>Filters.php</code>, add compensating server-side sanitization or allowlist logic in the receiving code.</p>
|
|
</li>
|
|
<li>
|
|
<strong>Test encoded attack strings too</strong>
|
|
<p>Because the filter canonicalizes input, test URL-encoded and HTML-entity-encoded payloads in addition to plain strings.</p>
|
|
</li>
|
|
<li>
|
|
<strong>Watch the logs when troubleshooting blocks</strong>
|
|
<p>The filter logs a structured critical event named <code>SECURITY_BLOCKED_REQUEST</code> with a payload hash and field name.</p>
|
|
</li>
|
|
</ol>
|
|
|
|
<h2 id="common-pitfalls">Common pitfalls</h2>
|
|
|
|
<table>
|
|
<thead>
|
|
<tr><th>Pitfall</th><th>Why it happens</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td>A rich text feature keeps returning 403</td>
|
|
<td>The submitted markup matches one of the high-confidence XSS patterns, and the route is still under the global filter.</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Input looks harmless in raw form but still gets blocked</td>
|
|
<td>The canonicalization step decoded the payload into a dangerous form before matching.</td>
|
|
</tr>
|
|
<tr>
|
|
<td>A developer adds an exception without extra protection</td>
|
|
<td>The route bypasses the global blocker and now depends entirely on downstream validation.</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Files are assumed to be covered here</td>
|
|
<td>Uploaded file contents are handled by the separate file-upload guard, not this filter.</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<div class="callout success">
|
|
<span>+</span>
|
|
<div>
|
|
<strong>Practical takeaway</strong>
|
|
Treat <code>SecurityInputFilter</code> as the first request-level XSS tripwire.
|
|
Keep it on by default, make exceptions rarely, and document every exception
|
|
with the safer validation path that replaces it.
|
|
</div>
|
|
</div>
|