Backend security rules based on SecureCodeWarrior AI Security Rules. Apply when reviewing or writing server-side code.
php artisan boost:add-skill pekral/arch-app-services
Save this content to: .cursor/rules/security/backend.md
## General Secure Coding Practices
- Validate and sanitize all user inputs to prevent injection attacks.
- Use error handling without revealing sensitive information.
- Avoid exposing sensitive data in API responses.
- Do not hardcode any secrets (credentials, API keys, etc) in the source code or configuration files.
## Safe Validation & Error Messages (issue #540)
Validation messages, exception messages, HTTP error bodies, log lines that reach end users, and every locale entry behind `__()` / `trans()` / `Lang::get()` / `@lang` are an attack surface — careless wording confirms account existence, reveals storage shape, or hints at the next probe. Apply these rules to **every user-facing string the diff adds, modifies, or translates**, in the default locale **and** in every other locale shipped with the project.
- **No identity / account enumeration.** Authentication, password-reset, sign-up, change-email, and any account-lookup endpoint must return one generic message for the whole flow — *"Invalid credentials."*, *"If the account exists, we sent the reset link."*, *"Unable to sign in."* — never a message that distinguishes *email not found* from *wrong password*, *account locked*, *email not verified*, *2FA required*, or *user disabled*. The decision branch must run server-side without leaking into the response body, the response status, the response latency, or the response shape (no field-level error on `email` only, no different envelope per branch).
- **No authorization granularity leaks.** Permission-denied responses must not reveal whether the resource exists. Return a generic `404 Not Found` (or the project's equivalent generic error) for both *missing* and *forbidden*; never *"You do not have permission to access invoice #42"* (confirms the row exists) or *"Project not found in your workspace"* (confirms it exists in another).
- **No internal implementation detail.** Do not surface stack traces, file paths, framework or library versions, fully-qualified class names, database table / column / index names, SQL fragments, ORM error envelopes, queue / cache driver identifiers, or feature-flag keys to end users. Strip them before the response leaves the application boundary; log the detail server-side instead.
- **No verbatim echo of attacker input.** Validation messages must not concatenate the rejected value into the response — *"Email '[email protected]' is invalid"* is reflective and aids phishing / XSS pivots. Reference the field by name only — *"The email address is invalid."* — and let the client display the user's input next to the message if needed.
- **No password / token policy leak beyond the stated rule.** Reject reasons must match the rule the user can read in the form ("Password must be at least 12 characters."). Do not reveal proximity to the rule ("only one character short", "almost matches the breach list", "this password was used 11 months ago"), do not state which character class is missing in a single response when multiple are missing (collapse into the stated requirement), and do not echo password fragments back.
- **No timing or shape side channels.** Generic messages lose their value when the response latency or the response body shape differs per branch. Equalize the server-side path before responding (constant-time compare on credential checks, identical envelope for *success / not found / forbidden* on enumeration-sensitive endpoints).
- **Translations carry the same contract.** When the diff adds a key in the default locale, **every other shipped locale must receive the same safe wording** — a translator must not reintroduce *"Account does not exist"* in `cs.json` after the English source said *"Invalid credentials."*. The CR translation-completeness walk owns missing-key findings; this rule owns wording findings on every locale that already carries the key.
- **Specificity stays on the safe surfaces.** Schema-level validation that does **not** leak existence — *"The age must be at least 18."*, *"Phone number is required."*, *"File must be a PDF under 5 MB."* — stays specific so users can fix their input; the above prohibitions cover the enumeration / authorization / introspection surfaces only.
## HTTP Security headers and Cookies
- Use a Content Security Policy (CSP) to protect against XSS and clickjacking attacks.
- Set cookies with `HttpOnly`, `Secure`, and `SameSite` attributes.
- Enforce strict CORS policies for cookies.
### CSRF Protection
Apply the following rules only if authentication relies on cookies instead of tokens.
- Use anti-CSRF tokens for state-changing operations.
- Validate `Origin` and `Referer` headers for non-GET requests.
- Require re-authentication before performing sensitive actions.
## Output Rendering
- Ensure output is encoded correctly for the corresponding context.
- Escape special characters in output to prevent injection attacks.
## Database
- Use parameterized queries or ORM to prevent injections.
- Implement proper authentication and authorization.
- Handle sensitive data properly.
- Monitor security issues.
- Apply the principle of least privilege to database users.
## API Security
- Apply authentication and integrity checks on all API requests.
- Configure CORS policies to restrict cross-origin access to trusted domains only.
- Apply rate limiting to manage traffic.
- Enforce security headers.
- Handle errors securely without revealing sensitive details to end users.
- Log access and actions for monitoring, auditing, and detecting abnormal activity.
## External Requests
- Restrict outbound requests to only necessary external services and internal endpoints.
- Use allowlists to define permitted destinations instead of blocking known bad domains.
- Disable unnecessary URL fetching capabilities in your application.
- Validate and sanitize all user-supplied URLs before making requests.
- Implement request timeouts and rate limits to prevent abuse.
## Malicious Code & Supply-Chain Indicators (issue #549)
Code that fetches remote content, executes processes, or handles transport security can hide attacker behaviour behind ordinary-looking flags. Treat the patterns below as high-signal indicators of malicious or compromised code and flag every match on a line the diff adds or modifies — in application code (`exec()`, `shell_exec()`, `system()`, `proc_open()`, backticks, `Process::run()`), in shell / deploy / CI scripts (`*.sh`, `Makefile`, `composer.json` / `package.json` script hooks, GitHub Actions steps), and in installer / post-install hooks. Each pattern is a finding unless the surrounding code documents an explicit, legitimate reason inline.
- **Silent remote fetch ("tichý curl").** `curl` / `wget` invoked with silence flags (`-s`, `-sS`, `--silent`, `-q`, `wget -q`), especially when the response is piped straight into an interpreter (`curl … | sh`, `curl … | bash`, `| php`, `| python`) or written to disk and executed. A silent download-and-execute is a remote-code-execution and supply-chain vector: the URL is attacker-controllable and the transfer leaves no trace in logs. Require the destination to be an allow-listed, version-pinned HTTPS URL with a verified checksum / signature, and never pipe a network response directly into a shell. Severity: **Critical** when piped to an interpreter or executed; **High** otherwise.
- **Disabled TLS validation ("ignorování TLS validace").** Any code or flag that turns off certificate or hostname verification: `curl -k` / `--insecure`, `wget --no-check-certificate`, PHP `CURLOPT_SSL_VERIFYPEER => false` / `CURLOPT_SSL_VERIFYHOST => 0`, Guzzle / HTTP-client `'verify' => false`, a `stream_context` with `verify_peer => false` / `verify_peer_name => false`, or `NODE_TLS_REJECT_UNAUTHORIZED=0`. Disabling TLS validation exposes every request to man-in-the-middle interception and payload tampering. Keep validation on; if a self-signed internal CA is genuinely required, pin its certificate bundle (`CURLOPT_CAINFO`, `'verify' => '/path/to/ca.pem'`) instead of disabling the check. Severity: **Critical** on any request carrying credentials, tokens, or downloaded executables; **High** otherwise.
- **Suppressed error output ("potlačení chybového výstupu").** Constructs that hide failures from logs and reviewers: shell redirects `2>/dev/null` / `&>/dev/null` / `2>&1 >/dev/null` on a command whose result matters; the PHP error-suppression operator `@` on a security-relevant call (`@file_get_contents`, `@unlink`, `@exec`); `error_reporting(0)` / `ini_set('display_errors', '0')` flipped inside application logic; and empty `catch {}` blocks that swallow an exception without logging or rethrowing. Suppressed output lets a failed integrity check, a rejected TLS handshake, or a failed command pass unnoticed. Require the error to be handled or logged through the project's error sink; never silence a security-relevant operation. Severity: **High**; **Critical** when the suppressed call performs a download, executes a process, or validates a signature / checksum.
- **Hidden file + detached background process ("skrytý soubor v /tmp a spuštění procesu na pozadí").** Writing to a world-writable or hidden path — `/tmp`, `/var/tmp`, `/dev/shm`, or any dot-prefixed filename (`/tmp/.x`) — combined with launching a detached or backgrounded process: a trailing `&`, `nohup … &`, `disown`, `setsid`, `at` / `cron` registration, `proc_open` / `exec` with `> /dev/null 2>&1 &`, or `register_shutdown_function` spawning a process. This is the classic dropper-plus-persistence pattern. Require temp files to use `tmpfile()` / `tempnam(sys_get_temp_dir(), …)` with `0600` permissions and deletion in a `finally`, and route background work through the project's queue / scheduler — never a raw detached shell process. Severity: **Critical** when both halves co-occur (hidden / temp write **and** background execution); **High** for either half alone on a security-relevant path.
Each finding maps to an active attack technique (RCE, MITM, evasion, persistence), so the **Suggested Fix** replaces the unsafe construct with the allow-listed / verified / logged / queued equivalent above. Genuinely benign uses — a `-s` curl inside a documentation example, a `2>/dev/null` on a best-effort cleanup annotated with a comment — are not findings when the intent is documented inline.
## Malicious File Upload Content (issue #680)
Uploaded files are an attacker-controlled byte stream. When the application later renders, serves, or processes the file's bytes, name, or metadata, active content inside the file can execute in the browser or be injected into downstream consumers. Apply these rules to every code path that **accepts**, **stores**, **processes**, or **serves** user-uploaded files.
> **Scope boundary — CONTENT / RENDER only.** This rule covers the CONTENT / RENDER surface of an uploaded file (active content executed when the file's bytes, name, or metadata are later rendered or served). The file TYPE / TRANSPORT surface (extension allow-list, declared-vs-actual MIME, magic-byte signature, double extension, path traversal, executability in webroot) stays with `security-review/SKILL.md` File Handling — **raise one finding per violation, never both**. A single upload sink that fails both surfaces produces the type/transport finding for the accept decision and the content/render finding for the output decision, on distinct lines; never two findings for the same line.
- **Stored XSS from file content.** Any file whose bytes are later rendered as HTML by a browser can carry an XSS payload — not only `.html` files but also `.svg`, `.xml`, `.txt` served without a strict `Content-Type`, or any format whose content is parsed and displayed. Never render the raw bytes of a user upload as `text/html`. Serve user uploads with `Content-Disposition: attachment` and, for inline previews, with a strict, narrowed `Content-Type` (e.g. `image/png`, never `text/html`). Set `X-Content-Type-Options: nosniff` on every upload-serving response. Severity: **Critical** when the rendered content is reachable by other users (stored XSS); **Moderate** for self-XSS or single-user surfaces.
- **SVG with active content served inline.** SVG is XML and may contain `<script>` elements, `onload` / `onerror` / `on*` event handlers, `<foreignObject>` embedding HTML, and CSS `url()` references. Serving an SVG inline (directly in a `<img src>` tag that loads the SVG and passes it to the browser's SVG renderer as `image/svg+xml` inline) or embedding it with `<embed>` / `<object>` gives it a document origin and allows script execution. Either sanitize SVG uploads server-side (strip `<script>`, all `on*` attributes, `<foreignObject>`, and `javascript:` URIs using a dedicated SVG sanitizer) and serve with a sandboxed origin, or serve SVG files only via `<img src>` (which blocks script execution) with `Content-Disposition: attachment` for direct download. Never serve a user-uploaded SVG as inline HTML. Severity: **Critical** when served to other users.
- **CSV / Excel formula injection.** Spreadsheet applications interpret cell values starting with `=`, `+`, `-`, `@`, or a tab / carriage-return prefix as formulas. A user-controlled cell value such as `=cmd|'/c calc'!A0` or `=HYPERLINK("http://attacker.example/","click me")` executes when the file is opened in a spreadsheet application. Prefix every user-supplied cell value that starts with `=`, `+`, `-`, `@`, `\t`, or `\r` with a single apostrophe (`'`) before writing it to any CSV or XLSX/ODS export, and instruct any import path to validate or reject such values before processing. Severity: **Moderate**.
- **HTML / JavaScript in filenames and metadata.** File names, EXIF tags, PDF metadata fields (Title, Author, Subject, Keywords), and similar user-controlled strings can contain `<script>`, `"><img onerror=...>`, or other injection payloads that execute if the application renders the value into HTML without escaping. Always HTML-encode (e.g. `htmlspecialchars()`) or framework-escape every display of a filename or metadata field; never interpolate it into a template raw. Also sanitize filenames before using them in paths, HTTP `Content-Disposition` headers, or log lines. Severity: **Critical** when unescaped output is rendered to other users; **Moderate** for single-user surfaces.
- **Polyglot files.** A single byte sequence can be simultaneously a valid image (JPEG, GIF, PNG header) and a valid HTML / JavaScript document. Browsers may execute a polyglot served from the same origin as a script or document even when the application believes it is serving only an image. Serve all user uploads from a dedicated, cookieless subdomain or storage origin (e.g. `uploads.example.com` instead of `app.example.com`), enforce `Content-Disposition: attachment` for direct downloads, set `X-Content-Type-Options: nosniff`, and apply a strict CSP on any origin that serves user content. Do not rely solely on extension or MIME validation to neutralise a polyglot — content isolation by origin is the primary control. Severity: **Critical** when uploads are served from the application origin without `nosniff` and a strict CSP.
- **Missing `Content-Disposition` / `nosniff` on upload serving endpoints.** Every endpoint that serves user-uploaded content must include `Content-Disposition: attachment` (or, for in-browser previews, a narrowed and validated `Content-Type`) and `X-Content-Type-Options: nosniff`. Absence of these headers allows browsers to sniff content and execute HTML or script payloads embedded in otherwise-innocent file types (e.g. a JPEG that passes magic-byte validation but contains an HTML payload). For user-uploaded files served inline (images in an `<img>` tag), the file must originate from a separate, sandboxed origin. Severity: **Moderate** (escalate to **Critical** when the file is served inline on the application origin with no CSP script restriction).
## Hidden / Invisible Characters in Stored Fields (issue #714)
User-controlled strings can carry invisible or deceptive Unicode that survives into a database column and harms a *later* consumer — the rendered page, a log line, a `UNIQUE` constraint, a CSV export, or another service that reads the row. This is an **input / storage-time** vulnerability, distinct from output encoding: the goal is that the **stored bytes are clean**, so normalization and stripping happen at the **write boundary**, not only at render. Apply to every code path that persists a user-controlled string to the database — Eloquent `create()` / `update()` / `fill()` / `save()` / mass assignment, query-builder `insert()` / `update()`, raw column writes, and the FormRequest / Data Validator that feeds them — and flag a user-controlled string written to a column without NFC normalization and invisible-character handling.
- **Zero-width and invisible characters.** `U+200B`–`U+200D` (ZWSP / ZWNJ / ZWJ), `U+2060` (word joiner), `U+FEFF` (BOM / ZWNBSP), `U+00AD` (soft hyphen), `U+180E`. Invisible on screen yet part of the stored value: they smuggle data past moderation, defeat `UNIQUE` constraints and equality / search lookups (two visually identical names differ by a hidden byte), and pad a value past a length check. Strip them — or reject the input — before persisting. Severity: **Moderate**; **Critical** when the field guards identity or an allow / deny decision (see *Severity* below).
- **Bidirectional control characters (persisted Trojan Source).** `U+202A`–`U+202E` (LRE / RLE / PDF / LRO / RLO) and `U+2066`–`U+2069` (LRI / RLI / FSI / PDI) reorder how text renders without changing its logical byte order. Stored in a display name, filename, comment, or description they are the persisted analogue of CVE-2021-42574 (Trojan Source): the value renders spoofed / reversed to every later viewer. Strip the bidi-control ranges before storing; never let an unbalanced override reach a column. Severity: **Moderate**; **Critical** on an identity / security-decision surface.
- **C0 / C1 control characters.** `U+0000`–`U+0008`, `U+000B`, `U+000C`, `U+000E`–`U+001F`, and `U+007F`–`U+009F` — allow only the whitespace the field legitimately needs (a tab / newline in a multi-line text field, never in a single-line identifier). A stored `NUL` truncates the value in C-backed consumers; other control bytes corrupt the stored value and feed downstream injection when the field is later emitted. Reject or strip the disallowed range at the write boundary. Severity: **Moderate**; **Critical** on an identity / security-decision surface.
- **Homoglyph / confusable / mixed-script identifiers and non-NFC values.** For any **identity-sensitive** field — username, email local-part, slug, domain, team / tenant name, or any column used in a uniqueness constraint or an allow / deny decision — a look-alike character (Cyrillic `а` for Latin `a`, Greek, fullwidth forms) or a non-normalized (non-NFC) form lets an attacker register a value that is visually identical to, yet byte-distinct from, an existing one — defeating the uniqueness guard and impersonating the victim. Normalize to **NFC** and enforce a single-script / confusable-skeleton check (or a strict allow-listed charset) **before** the uniqueness check and the write. Severity: **Critical** (identity spoofing / constraint bypass).
**Required control.** Normalize user-controlled strings to **NFC** (`Normalizer::normalize($value, Normalizer::FORM_C)` / `normalizer_normalize()`) and strip / reject the disallowed invisible, bidi-control, and C0 / C1 ranges at the **input boundary** — a reusable validation rule, a Data Validator, or an attribute cast / mutator applied **centrally**, never ad-hoc per call site — so every later consumer reads clean bytes. The **Suggested Fix** introduces that central sanitizer / normalizer and routes the field through it; identity-sensitive fields additionally get the single-script / confusable check before the uniqueness lookup. Stripping only at render is not sufficient — the database must never hold the dangerous bytes.
**Severity.** **Critical** when the field drives a security decision — authentication / login identifier, authorization key, allow / deny list, identity uniqueness constraint, or financial reference — and no normalization / stripping is applied. **Moderate** for general user-content fields (display name, comment, description) where the impact is spoofing-on-render or data-integrity drift.
> **Scope boundary — INPUT / STORAGE only.** This rule owns the *stored byte content* of a database field: NFC normalization plus invisible / bidi / C0 / C1 stripping at write time. It does **not** own output encoding / escaping (XSS) — that stays with *Output Rendering* and the frontend escaping rules — nor file-content rendering, which stays with *Malicious File Upload Content (issue #680)*. It also does **not** own the *emission* surface — a stored control byte later written to a log, header, or console is owned by the log-injection check (`security-review/SKILL.md` Security Logging & Monitoring); this rule owns only the persisted bytes in the column. When the same user string is both stored unsanitized **and** rendered unescaped (or emitted to a log), raise the storage finding here for the write and let the output-encoding / log-injection rule own the render / emission — one finding per surface, never two for the same line.
Validation messages, exception messages, HTTP error bodies, log lines that reach end users, and every locale entry behind __() / trans() / Lang::get() / @lang are an attack surface — careless wording confirms account existence, reveals storage shape, or hints at the next probe. Apply these rules to every user-facing string the diff adds, modifies, or translates, in the default locale and in every other locale shipped with the project.
email only, no different envelope per branch).404 Not Found (or the project's equivalent generic error) for both missing and forbidden; never "You do not have permission to access invoice #42" (confirms the row exists) or "Project not found in your workspace" (confirms it exists in another).cs.json after the English source said "Invalid credentials.". The CR translation-completeness walk owns missing-key findings; this rule owns wording findings on every locale that already carries the key.HttpOnly, Secure, and SameSite attributes.Apply the following rules only if authentication relies on cookies instead of tokens.
Origin and Referer headers for non-GET requests.Code that fetches remote content, executes processes, or handles transport security can hide attacker behaviour behind ordinary-looking flags. Treat the patterns below as high-signal indicators of malicious or compromised code and flag every match on a line the diff adds or modifies — in application code (exec(), shell_exec(), system(), proc_open(), backticks, Process::run()), in shell / deploy / CI scripts (*.sh, Makefile, composer.json / package.json script hooks, GitHub Actions steps), and in installer / post-install hooks. Each pattern is a finding unless the surrounding code documents an explicit, legitimate reason inline.
curl / wget invoked with silence flags (-s, -sS, --silent, -q, wget -q), especially when the response is piped straight into an interpreter (curl … | sh, curl … | bash, | php, | python) or written to disk and executed. A silent download-and-execute is a remote-code-execution and supply-chain vector: the URL is attacker-controllable and the transfer leaves no trace in logs. Require the destination to be an allow-listed, version-pinned HTTPS URL with a verified checksum / signature, and never pipe a network response directly into a shell. Severity: Critical when piped to an interpreter or executed; High otherwise.curl -k / --insecure, wget --no-check-certificate, PHP CURLOPT_SSL_VERIFYPEER => false / CURLOPT_SSL_VERIFYHOST => 0, Guzzle / HTTP-client 'verify' => false, a stream_context with verify_peer => false / verify_peer_name => false, or NODE_TLS_REJECT_UNAUTHORIZED=0. Disabling TLS validation exposes every request to man-in-the-middle interception and payload tampering. Keep validation on; if a self-signed internal CA is genuinely required, pin its certificate bundle (CURLOPT_CAINFO, 'verify' => '/path/to/ca.pem') instead of disabling the check. Severity: Critical on any request carrying credentials, tokens, or downloaded executables; High otherwise.2>/dev/null / &>/dev/null / 2>&1 >/dev/null on a command whose result matters; the PHP error-suppression operator @ on a security-relevant call (@file_get_contents, @unlink, @exec); error_reporting(0) / ini_set('display_errors', '0') flipped inside application logic; and empty catch {} blocks that swallow an exception without logging or rethrowing. Suppressed output lets a failed integrity check, a rejected TLS handshake, or a failed command pass unnoticed. Require the error to be handled or logged through the project's error sink; never silence a security-relevant operation. Severity: High; Critical when the suppressed call performs a download, executes a process, or validates a signature / checksum./tmp, /var/tmp, /dev/shm, or any dot-prefixed filename (/tmp/.x) — combined with launching a detached or backgrounded process: a trailing &, nohup … &, disown, setsid, at / cron registration, proc_open / exec with > /dev/null 2>&1 &, or register_shutdown_function spawning a process. This is the classic dropper-plus-persistence pattern. Require temp files to use tmpfile() / tempnam(sys_get_temp_dir(), …) with 0600 permissions and deletion in a finally, and route background work through the project's queue / scheduler — never a raw detached shell process. Severity: Critical when both halves co-occur (hidden / temp write and background execution); High for either half alone on a security-relevant path.Each finding maps to an active attack technique (RCE, MITM, evasion, persistence), so the Suggested Fix replaces the unsafe construct with the allow-listed / verified / logged / queued equivalent above. Genuinely benign uses — a -s curl inside a documentation example, a 2>/dev/null on a best-effort cleanup annotated with a comment — are not findings when the intent is documented inline.
Uploaded files are an attacker-controlled byte stream. When the application later renders, serves, or processes the file's bytes, name, or metadata, active content inside the file can execute in the browser or be injected into downstream consumers. Apply these rules to every code path that accepts, stores, processes, or serves user-uploaded files.
Scope boundary — CONTENT / RENDER only. This rule covers the CONTENT / RENDER surface of an uploaded file (active content executed when the file's bytes, name, or metadata are later rendered or served). The file TYPE / TRANSPORT surface (extension allow-list, declared-vs-actual MIME, magic-byte signature, double extension, path traversal, executability in webroot) stays with
security-review/SKILL.mdFile Handling — raise one finding per violation, never both. A single upload sink that fails both surfaces produces the type/transport finding for the accept decision and the content/render finding for the output decision, on distinct lines; never two findings for the same line.
.html files but also .svg, .xml, .txt served without a strict Content-Type, or any format whose content is parsed and displayed. Never render the raw bytes of a user upload as text/html. Serve user uploads with Content-Disposition: attachment and, for inline previews, with a strict, narrowed Content-Type (e.g. image/png, never text/html). Set X-Content-Type-Options: nosniff on every upload-serving response. Severity: Critical when the rendered content is reachable by other users (stored XSS); Moderate for self-XSS or single-user surfaces.<script> elements, onload / onerror / on* event handlers, <foreignObject> embedding HTML, and CSS url() references. Serving an SVG inline (directly in a <img src> tag that loads the SVG and passes it to the browser's SVG renderer as image/svg+xml inline) or embedding it with <embed> / <object> gives it a document origin and allows script execution. Either sanitize SVG uploads server-side (strip <script>, all on* attributes, <foreignObject>, and javascript: URIs using a dedicated SVG sanitizer) and serve with a sandboxed origin, or serve SVG files only via <img src> (which blocks script execution) with Content-Disposition: attachment for direct download. Never serve a user-uploaded SVG as inline HTML. Severity: Critical when served to other users.=, +, -, @, or a tab / carriage-return prefix as formulas. A user-controlled cell value such as =cmd|'/c calc'!A0 or =HYPERLINK("http://attacker.example/","click me") executes when the file is opened in a spreadsheet application. Prefix every user-supplied cell value that starts with =, +, -, @, \t, or \r with a single apostrophe (') before writing it to any CSV or XLSX/ODS export, and instruct any import path to validate or reject such values before processing. Severity: Moderate.<script>, "><img onerror=...>, or other injection payloads that execute if the application renders the value into HTML without escaping. Always HTML-encode (e.g. htmlspecialchars()) or framework-escape every display of a filename or metadata field; never interpolate it into a template raw. Also sanitize filenames before using them in paths, HTTP Content-Disposition headers, or log lines. Severity: Critical when unescaped output is rendered to other users; Moderate for single-user surfaces.uploads.example.com instead of app.example.com), enforce Content-Disposition: attachment for direct downloads, set X-Content-Type-Options: nosniff, and apply a strict CSP on any origin that serves user content. Do not rely solely on extension or MIME validation to neutralise a polyglot — content isolation by origin is the primary control. Severity: Critical when uploads are served from the application origin without nosniff and a strict CSP.Content-Disposition / nosniff on upload serving endpoints. Every endpoint that serves user-uploaded content must include Content-Disposition: attachment (or, for in-browser previews, a narrowed and validated Content-Type) and X-Content-Type-Options: nosniff. Absence of these headers allows browsers to sniff content and execute HTML or script payloads embedded in otherwise-innocent file types (e.g. a JPEG that passes magic-byte validation but contains an HTML payload). For user-uploaded files served inline (images in an <img> tag), the file must originate from a separate, sandboxed origin. Severity: Moderate (escalate to Critical when the file is served inline on the application origin with no CSP script restriction).User-controlled strings can carry invisible or deceptive Unicode that survives into a database column and harms a later consumer — the rendered page, a log line, a UNIQUE constraint, a CSV export, or another service that reads the row. This is an input / storage-time vulnerability, distinct from output encoding: the goal is that the stored bytes are clean, so normalization and stripping happen at the write boundary, not only at render. Apply to every code path that persists a user-controlled string to the database — Eloquent create() / update() / fill() / save() / mass assignment, query-builder insert() / update(), raw column writes, and the FormRequest / Data Validator that feeds them — and flag a user-controlled string written to a column without NFC normalization and invisible-character handling.
U+200B–U+200D (ZWSP / ZWNJ / ZWJ), U+2060 (word joiner), U+FEFF (BOM / ZWNBSP), U+00AD (soft hyphen), U+180E. Invisible on screen yet part of the stored value: they smuggle data past moderation, defeat UNIQUE constraints and equality / search lookups (two visually identical names differ by a hidden byte), and pad a value past a length check. Strip them — or reject the input — before persisting. Severity: Moderate; Critical when the field guards identity or an allow / deny decision (see Severity below).U+202A–U+202E (LRE / RLE / PDF / LRO / RLO) and U+2066–U+2069 (LRI / RLI / FSI / PDI) reorder how text renders without changing its logical byte order. Stored in a display name, filename, comment, or description they are the persisted analogue of CVE-2021-42574 (Trojan Source): the value renders spoofed / reversed to every later viewer. Strip the bidi-control ranges before storing; never let an unbalanced override reach a column. Severity: Moderate; Critical on an identity / security-decision surface.U+0000–U+0008, U+000B, U+000C, U+000E–U+001F, and U+007F–U+009F — allow only the whitespace the field legitimately needs (a tab / newline in a multi-line text field, never in a single-line identifier). A stored NUL truncates the value in C-backed consumers; other control bytes corrupt the stored value and feed downstream injection when the field is later emitted. Reject or strip the disallowed range at the write boundary. Severity: Moderate; Critical on an identity / security-decision surface.а for Latin a, Greek, fullwidth forms) or a non-normalized (non-NFC) form lets an attacker register a value that is visually identical to, yet byte-distinct from, an existing one — defeating the uniqueness guard and impersonating the victim. Normalize to NFC and enforce a single-script / confusable-skeleton check (or a strict allow-listed charset) before the uniqueness check and the write. Severity: Critical (identity spoofing / constraint bypass).Required control. Normalize user-controlled strings to NFC (Normalizer::normalize($value, Normalizer::FORM_C) / normalizer_normalize()) and strip / reject the disallowed invisible, bidi-control, and C0 / C1 ranges at the input boundary — a reusable validation rule, a Data Validator, or an attribute cast / mutator applied centrally, never ad-hoc per call site — so every later consumer reads clean bytes. The Suggested Fix introduces that central sanitizer / normalizer and routes the field through it; identity-sensitive fields additionally get the single-script / confusable check before the uniqueness lookup. Stripping only at render is not sufficient — the database must never hold the dangerous bytes.
Severity. Critical when the field drives a security decision — authentication / login identifier, authorization key, allow / deny list, identity uniqueness constraint, or financial reference — and no normalization / stripping is applied. Moderate for general user-content fields (display name, comment, description) where the impact is spoofing-on-render or data-integrity drift.
Scope boundary — INPUT / STORAGE only. This rule owns the stored byte content of a database field: NFC normalization plus invisible / bidi / C0 / C1 stripping at write time. It does not own output encoding / escaping (XSS) — that stays with Output Rendering and the frontend escaping rules — nor file-content rendering, which stays with Malicious File Upload Content (issue #680). It also does not own the emission surface — a stored control byte later written to a log, header, or console is owned by the log-injection check (
security-review/SKILL.mdSecurity Logging & Monitoring); this rule owns only the persisted bytes in the column. When the same user string is both stored unsanitized and rendered unescaped (or emitted to a log), raise the storage finding here for the write and let the output-encoding / log-injection rule own the render / emission — one finding per surface, never two for the same line.
How can I help you explore Laravel packages today?