Series 8 — Part 2 of 7

Embedding JSON in HTML attributes is common and dangerous. json_encode() output in an onclick handler produces syntactically broken HTML if the JSON contains a double quote — and creates an XSS vector if it contains an angle bracket. The fix is two function calls, not one.

The Problem

// BROKEN: json_encode alone in HTML attributes
$data  = ['name' => 'O\'Brien & Associates', 'id' => 42];
$json  = json_encode($data);
// $json = {"name":"O'Brien & Associates","id":42}

echo '';
// Output: 
//                                           ^ the { breaks the attribute boundary

The Fix: ENT_QUOTES + JSON_HEX_TAG

function json_attr(mixed $data): string
{
    // Step 1: Encode to JSON, escaping HTML-special chars in JSON strings
    $json = json_encode($data, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP | JSON_THROW_ON_ERROR);
    // JSON_HEX_TAG converts < and > to < and > — prevents script injection
    // JSON_HEX_QUOT converts " to " — prevents attribute boundary break
    // JSON_HEX_AMP converts & to & — prevents HTML entity confusion

    // Step 2: HTML-encode the result for safe insertion into an HTML attribute
    return htmlspecialchars($json, ENT_QUOTES | ENT_HTML5, 'UTF-8');
}

// Usage
echo '';
// Safe output — the onclick attribute value is properly escaped

When to Use JSON in HTML Data Attributes Instead

A cleaner pattern for complex data is data-* attributes with JavaScript parsing:

// Safer: data attribute, no onclick inline handler
echo '';
// In JS: parse once from the data attribute
document.querySelectorAll('.js-action-btn').forEach(btn => {
    btn.addEventListener('click', () => {
        const payload = JSON.parse(btn.dataset.payload);
        handleClick(payload);
    });
});

This pattern separates data from behaviour and is compatible with Content Security Policy's script-src 'self' (inline event handlers are not).

How to Catch This Before Production

# Grep for json_encode in HTML contexts without the HEX flags
grep -rn "json_encode" src/ | grep -v "JSON_HEX_TAG\|json_attr"

What to Watch For

  • JSON in script tags — Embedding JSON in <script> blocks (not attributes) has different escaping rules. Use JSON_HEX_TAG to prevent </script> in a JSON string from closing the tag early.
  • CSP and inline handlers — If you have a Content Security Policy, onclick="..." attributes require 'unsafe-inline'. Data attributes + event listeners don't. Prefer the data attribute pattern.