nhance/app/Views/docs/input-security.php
2026-05-18 12:28:19 +05:30

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>&lt;script</code>, <code>&lt;/script&gt;</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>&lt;iframe</code>, <code>&lt;object</code>, <code>&lt;embed</code>, <code>&lt;applet</code>, <code>&lt;img</code></td>
</tr>
<tr>
<td>SVG / MathML vectors</td>
<td><code>&lt;svg</code>, <code>&lt;math</code></td>
</tr>
<tr>
<td>Meta refresh payloads</td>
<td><code>&lt;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>