# A2UI Protocol Security Audit — 2026-04-01

Auditor: Claude Opus 4.6 (security-sentinel)
Scope: A2UI renderer, extraction, preview controller, Blade templates, MCP tool

## Executive Summary

The A2UI renderer is **well-implemented** with solid baseline sanitization. Text escaping via `htmlspecialchars()`, URL scheme allowlisting, CSS color validation, and cycle detection are all correctly applied. However, several medium-severity issues exist that should be addressed.

**Overall risk: LOW-MEDIUM** — no critical XSS bypass found in the renderer itself. The most serious issues are in adjacent code (ArtifactPreviewController's raw HTML rendering, Tailwind CDN injection vector, and missing resource exhaustion limits).

---

## Findings

### FINDING 1 — ArtifactPreviewController renders raw HTML without sanitization
**Severity: HIGH**
**File:** `base/app/Http/Controllers/ArtifactPreviewController.php:41-47`

```php
private function renderHtml(string $content): Response
{
    return response($content, 200)
        ->header('Content-Type', 'text/html; charset=utf-8')
        ...
}
```

The `renderHtml()` method returns artifact HTML content verbatim with no sanitization. If an LLM agent generates an HTML artifact containing `<script>alert(1)</script>`, it renders as-is. While not A2UI-specific, this endpoint is in the same controller and shares the same route prefix. The `X-Frame-Options: SAMEORIGIN` header does not prevent XSS.

**Impact:** Full stored XSS. Any agent that creates an HTML artifact can execute JavaScript when a user previews it.

**Remediation:**
- Add `Content-Security-Policy: default-src 'self'; script-src 'none'; style-src 'unsafe-inline'` header to `renderHtml()`.
- Alternatively, serve HTML artifacts in a sandboxed iframe with `sandbox` attribute.

---

### FINDING 2 — Tailwind CDN script tag allows CSS injection escalation
**Severity: MEDIUM**
**File:** `base/app/Http/Controllers/ArtifactPreviewController.php:111-115`

```php
$body = <<<HTML
<script src="https://cdn.tailwindcss.com"></script>
<script>tailwindcss.config = { darkMode: 'class' }</script>
<div class="max-w-3xl mx-auto py-6">{$html}</div>
HTML;
```

The Tailwind CDN play script (`cdn.tailwindcss.com`) processes CSS utility classes at runtime. While the A2UI renderer restricts class names to predefined Tailwind utilities, a malicious component could inject arbitrary class names through the `gap` property in `renderRow()`:

```php
$gap = isset($props['gap']) ? 'gap-'.(int) $props['gap'] : 'gap-3';
```

The `(int)` cast here is safe, but the broader concern is that the Tailwind CDN processes ALL classes in the DOM — if future renderer changes introduce unsanitized class values, the CDN would generate arbitrary CSS from them. The CDN script itself is a third-party resource loaded without SRI (Subresource Integrity).

**Impact:** If the CDN is compromised or if class injection is introduced, arbitrary CSS could be generated. CSS-based data exfiltration is possible in some browsers.

**Remediation:**
- Add SRI hash to the Tailwind CDN script tag: `<script src="https://cdn.tailwindcss.com" integrity="sha384-..." crossorigin="anonymous">`
- Consider using a pre-built Tailwind CSS file instead of the runtime CDN.

---

### FINDING 3 — No component count or depth limit (resource exhaustion)
**Severity: MEDIUM**
**File:** `base/app/Infrastructure/A2ui/A2uiRenderer.php:27-39`

The renderer processes all components without limiting:
- Total component count (an LLM could produce 100,000+ components)
- Nesting depth (deep Column > Column > Column chains are only bounded by PHP's stack limit)

While cycle detection prevents infinite loops, a large acyclic graph will still consume unbounded CPU and memory.

**Impact:** Denial of service via large A2UI payloads. An LLM could be prompted to generate enormous surfaces.

**Remediation:**
```php
private const MAX_COMPONENTS = 500;
private const MAX_DEPTH = 20;

public function render(array $components, ...): HtmlString
{
    if (count($components) > self::MAX_COMPONENTS) {
        return new HtmlString('<!-- A2UI: component limit exceeded -->');
    }
    // ...
}
```
Add a `$depth` parameter to `renderNode()` and bail at MAX_DEPTH.

---

### FINDING 4 — Image/Video/Audio src allows any HTTP(S) URL (SSRF potential)
**Severity: LOW**
**File:** `base/app/Infrastructure/A2ui/A2uiRenderer.php:713-727`

```php
private function sanitizeUrl(string $url): string
{
    $scheme = strtolower(parse_url($url, PHP_URL_SCHEME) ?? '');
    if ($scheme !== '' && ! in_array($scheme, ['http', 'https'])) {
        return '';
    }
    return $this->e($url);
}
```

The URL sanitizer correctly blocks `javascript:`, `data:`, and other dangerous schemes. However, it allows any HTTP(S) URL including internal network addresses (e.g., `http://169.254.169.254/`, `http://localhost:9000/`). Since these are rendered as `<img src>`, `<video src>`, and `<audio src>` in the browser, the browser (not the server) makes the request, so server-side SSRF is not possible. However, this can be used for:
- Internal network scanning via timing side-channels (image load events)
- Tracking pixels targeting the viewing user

**Impact:** Low. Browser-side only; no server-side SSRF.

**Remediation:** Consider documenting this as accepted risk, or add a domain allowlist for media URLs.

---

### FINDING 5 — `{!! $html !!}` in Blade is safe but fragile
**Severity: LOW (informational)**
**File:** `base/resources/views/components/a2ui/surface.blade.php:10`

```blade
{!! $html !!}
```

This unescaped output is safe because `A2uiRenderer::render()` returns an `HtmlString` with all user content escaped via `htmlspecialchars()`. However, this creates a **security invariant**: if any future renderer method forgets to call `$this->e()`, the XSS would flow directly to the user.

**Remediation:** Add a comment documenting this invariant:
```blade
{{-- SECURITY: $html is pre-escaped by A2uiRenderer. All text content uses htmlspecialchars(). --}}
{!! $html !!}
```

---

### FINDING 6 — No ReDoS risk in current regex patterns
**Severity: NONE (verified safe)**

Checked all regex patterns:
- `sanitizeColor`: `/^#[0-9a-fA-F]{3,8}$/` and `/^[a-zA-Z]+$/` — anchored, no backtracking
- `initials`: `/\s+/` — simple character class
- `extractA2uiSurfaces`: `/```a2ui\s*\n(.*?)\n```/s` — lazy quantifier with `\n` anchors, bounded

No ReDoS vectors found.

---

### FINDING 7 — Data binding traversal is safe
**Severity: NONE (verified safe)**

`resolveBindings()` and `resolveJsonPointer()` correctly implement RFC 6901 JSON Pointer resolution. The data model is provided by the calling code (not user-controlled in the A2UI context). The recursive `resolveBindings()` only enters nested arrays, with no prototype pollution risk (PHP arrays are not objects).

---

### FINDING 8 — MCP tool output is safely encapsulated
**Severity: NONE (verified safe)**
**File:** `base/app/Mcp/Tools/A2ui/A2uiRenderSurfaceTool.php:47`

The MCP tool returns rendered HTML inside a JSON string (`Response::text(json_encode(...))`). The HTML is JSON-encoded, preventing injection into MCP transport. Consumers must explicitly extract and render it.

---

## Risk Matrix

| # | Finding | Severity | Exploitability | Fix Effort |
|---|---------|----------|---------------|------------|
| 1 | Raw HTML artifact rendering | HIGH | Easy (any agent can create HTML artifacts) | Low |
| 2 | Tailwind CDN without SRI | MEDIUM | Requires CDN compromise | Low |
| 3 | No component/depth limits | MEDIUM | Easy (prompt LLM to generate large surface) | Low |
| 4 | Unrestricted media URLs | LOW | Easy but impact is minimal | Low |
| 5 | Fragile `{!! !!}` invariant | LOW | Requires future code regression | Trivial |

## Remediation Roadmap

1. **Immediate (Finding 1):** Add CSP headers to `renderHtml()` to block script execution: `Content-Security-Policy: script-src 'none'`
2. **Short-term (Finding 3):** Add MAX_COMPONENTS (500) and MAX_DEPTH (20) limits to the renderer
3. **Short-term (Finding 2):** Add SRI hash to Tailwind CDN script tag
4. **Documentation (Findings 5, 4):** Add security comments to Blade template; document media URL policy

## Verified Secure

- Text escaping: `htmlspecialchars(ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')` — correct, covers all XSS vectors
- URL scheme allowlist: only `http`, `https`, and relative — blocks `javascript:`, `data:`, `vbscript:`
- CSS color sanitization: hex and named colors only — blocks `expression()`, `url()`, `var()`
- Cycle detection: `$visited` array prevents infinite recursion in adjacency list traversal
- Unknown components: render as HTML comments with escaped type name
- Integer casting: `(int)` for padding, gap, dimensions — blocks injection in numeric contexts
- Input type allowlist: TextField and DateTimeInput restrict to safe HTML input types
