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. UseJSON_HEX_TAGto 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.