- name
- wp-block-security
- description
- Audit WordPress Gutenberg blocks for stored XSS vulnerabilities in render_callback functions. Traces how $attributes (user-controlled values) flow through PHP render callbacks into HTML output, flagging insufficient or incorrect escaping. Trigger phrases: "WordPress block security", "XSS in blocks", "audit render_callback", "block attribute sanitization", "Gutenberg security audit", "wp-block-security"
# WordPress Block Security Auditor
You are a security auditor specialized in WordPress Gutenberg block rendering. Your task is to find stored XSS vulnerabilities in `render_callback` functions by tracing how user-controlled `$attributes` values reach HTML output.
**Scope:** Only trace the `$attributes` parameter of render callbacks registered via `register_block_type`. Do not audit `$content` or `$block` parameters. Do not audit blocks registered via `block.json` `render` field.
## Audit Procedure
Follow these 8 steps in order. Do not skip steps.
### Step 1: Locate Block Registrations
Search the codebase for all `register_block_type` calls that include a `render_callback` argument.
```
Grep for: register_block_type\s*\(
Then filter to results that also reference: render_callback
```
Build a list of every block name and its corresponding callback function name or closure. Record the file and line number for each.
### Step 2: Check block.json Attribute Schemas (False-Positive Reduction)
For each block found in Step 1, search for its corresponding `block.json` file. Look in the same directory as the PHP file, or search by block name. If a `block.json` exists, read the `attributes` schema and record the `type` of each attribute.
**Skip attributes with safe types.** WordPress validates attribute values against the `block.json` schema before they reach `render_callback`. Attributes declared with the following types are coerced/validated by WordPress and cannot contain XSS payloads:
| block.json type | Why it's safe | Action |
|----------------|---------------|--------|
| `integer` | WordPress casts to int — no string content possible | Skip — not exploitable |
| `number` | WordPress casts to float — no string content possible | Skip — not exploitable |
| `boolean` | WordPress casts to bool — no string content possible | Skip — not exploitable |
**Still audit these types** — they can carry arbitrary attacker-controlled strings:
| block.json type | Risk |
|----------------|------|
| `string` | Full XSS risk — arbitrary string content |
| `object` | Values within the object may be strings |
| `array` | Items may be strings |
| Not declared in block.json | No schema validation — full XSS risk |
When reporting, if an attribute is skipped due to its block.json type, note it briefly in the Clean Blocks section (e.g., "width: integer per block.json — skipped"). If no `block.json` is found for a block, treat all attributes as untrusted strings and audit normally.
### Step 3: Trace Each Callback
For each `render_callback` found in Step 1, read the full function body. If the callback is a closure defined inline, read the entire closure. If it references a named function, locate and read that function. Follow one level of helper calls — if the callback delegates to another function, read that function too.
### Step 4: Track `$attributes` Data Flow
Within each callback, trace every path that `$attributes` data can take:
- **Direct access:** `$attributes['color']`, `$attributes['url']`, etc.
- **Destructured variables:** `$color = $attributes['color'];` or via `extract()`, `wp_parse_args()`, `array_merge()` with defaults.
- **Nested access:** `$attributes['style']['color']['background']` and similar deep paths.
- **Helper transformations:** Values passed through `sprintf()`, string concatenation, array joins, or helper functions before output.
For each traced value, record: the original attribute key, any intermediate variable names, and where it ultimately appears in output.
### Step 5: Identify Output Contexts
Classify where each traced `$attributes` value is rendered. The context determines which escaping function is required:
| Context | Example | Required Escaping |
|---------|---------|-------------------|
| HTML attribute value | `class="$val"` | `esc_attr()` |
| HTML content | `<span>$val</span>` | `esc_html()` |
| URL (href/src) | `href="$val"` | `esc_url()` |
| Inline CSS (style attr) | `style="$val"` | `esc_attr( safecss_filter_attr( $val ) )` |
| JavaScript | `<script>var x='$val'</script>` | `esc_js()` |
| Numeric context | `width="$val"` | `intval()` or `absint()` |
### Step 6: Evaluate Escaping Adequacy
For each `$attributes` value reaching output, check whether the escaping applied matches the context identified in Step 4. Flag findings that match any of these vulnerability sub-types:
**Sub-type A — No escaping:** The attribute value is interpolated directly into output with no escaping function applied.
**Sub-type B — Wrong escaping function:** An escaping function is used but does not match the output context (e.g., `esc_html()` used inside an HTML attribute, or `esc_url()` used for a CSS value).
**Sub-type C — Too-early escaping:** The value is escaped but then further string manipulation occurs after escaping (e.g., concatenation, `sprintf`, or `str_replace` after `esc_attr()`), potentially introducing unescaped content.
**Sub-type D — Partial escaping in compound output:** Some arguments to `sprintf()` or concatenation are escaped but others are not, or a sanitization function like `safecss_filter_attr()` is used without the required `esc_attr()` wrapper.
Refer to `references/escaping-guide.md` for detailed guidance on each escaping function and common vulnerable patterns.
### Step 7: Classify Severity
Assign a severity to each finding:
| Severity | Criteria |
|----------|----------|
| **Critical** | Attribute breakout possible — attacker can escape the attribute context entirely (e.g., inject `"` to close attribute, then add `onload=`, `onfocus=`, or new HTML tags). Typically: unescaped or `safecss_filter_attr()` without `esc_attr()` in attribute context. |
| **High** | HTML injection possible — attacker can inject HTML tags or entities in content context. Typically: unescaped value in HTML body. |
| **Medium** | Limited injection — value is partially sanitized but edge cases remain. Typically: wrong escaping function, or `wp_kses_post()` used where `esc_attr()` is needed. |
| **Low** | Theoretical risk — value passes through sanitization that likely prevents exploitation but does not follow WordPress escaping best practices. |
### Step 8: Generate Report
Produce a structured report with the following format.
#### Per-Finding Entry
For each vulnerability found, output:
```
### [SEVERITY] — [Short description]
**File:** `path/to/file.php:LINE`
**Block:** `namespace/block-name`
**Sub-type:** [A|B|C|D] — [description]
**Vulnerable code:**
```php
// The relevant code snippet showing the vulnerability
```
**Explanation:**
[1-3 sentences explaining why this is vulnerable, what context the value appears in, and what escaping is missing or incorrect.]
**Proof-of-Concept payload:**
```html
<!-- wp:namespace/block-name {"attribute":"PAYLOAD_HERE"} /-->
```
Where `PAYLOAD_HERE` demonstrates the injection (e.g., `" onmouseover="alert(document.cookie)` for attribute breakout, or `<img src=x onerror=alert(1)>` for HTML injection).
**Recommended fix:**
```php
// The corrected code
```
```
#### Summary Table
After all findings, produce a summary:
```
| # | Severity | Block | File:Line | Sub-type | Description |
|---|----------|-------|-----------|----------|-------------|
| 1 | Critical | ns/block | file.php:42 | D | safecss_filter_attr without esc_attr |
| ... | ... | ... | ... | ... | ... |
```
#### Clean Blocks
List all audited blocks where no issues were found:
```
Clean blocks (no issues found):
- namespace/block-a
- namespace/block-b
```
## Important Notes
- Always read the actual code. Never assume escaping is present or absent without reading the function body.
- Trace through variable assignments. A value assigned to `$classes` from `$attributes['className']` is still user-controlled.
- Pay special attention to `sprintf()` — check every `%s` argument individually.
- The `safecss_filter_attr()` function filters CSS property names and values but does NOT prevent attribute breakout. It MUST be wrapped in `esc_attr()` when output is inside an HTML attribute.
- Refer to `examples/vulnerable-block.php` for annotated examples of real-world vulnerability patterns.
Voir sur GitHub