JSON-LD Injection Prevention: Secure Your Structured Data
Stop JSON-LD injection attacks by sanitizing structured data. Learn how to prevent XSS and data poisoning in your Node.js and PHP applications today.
When I first started adding Schema.org markup to our marketing pages, I treated it like simple metadata. I just concatenated a few strings, wrapped them in a <script type="application/ld+json"> tag, and called it a day. It took an internal audit to realize that I had essentially created a wide-open vector for XSS. If a user can inject content into your structured data, they can break out of the JSON context and execute arbitrary JavaScript in the victim's browser.
Understanding JSON-LD Injection Risks
The core problem is that many developers treat JSON-LD as static text. When you include user-generated content—like a product review, a username, or a meta description—directly into that JSON block, you’re playing with fire.
If your output looks like this:
HTMLstyle="color:#808080"><style="color:#4EC9B0">script type="application/ld+json"> { "@context": "https://schema.org", "@type": "Product", "name": "<?php echo $user_input; ?>" } style="color:#808080"></style="color:#4EC9B0">script>
An attacker doesn't need to be a genius to break out. By providing </script><script>alert('XSS')</script>, they terminate your script tag early and force the browser to execute their payload. It’s a classic case of failing to treat structured data as a sensitive execution context. This is fundamentally different from XSS Prevention: Mastering Context-Aware Template Sanitization, where you might be used to HTML-encoding user input. HTML encoding inside a JSON block will actually break your JSON-LD, making it invalid for search engines.
The Strategy: JSON Serialization over String Concatenation
The fix isn't to encode characters; it's to use the language's native JSON serialization tools. Never, under any circumstances, build your JSON-LD by hand using string concatenation or template literals.
In Node.js, you should construct a clean JavaScript object and then use JSON.stringify(). If you're using Express, you might be tempted to pass this directly into a template. Instead, serialize it on the server and pass the resulting string to your template engine, ensuring the engine knows to treat it as raw text rather than HTML.
Implementation in Node.js
JAVASCRIPTconst structuredData = { "@context": "https://schema.org", "@type": "Product", "name": userInput // Let the library handle escaping }; // Use a replacer to handle potential circular references if necessary const jsonLd = JSON.stringify(structuredData);
Implementation in PHP
PHP’s json_encode is your best friend here. It handles character escaping automatically, including the characters that could break out of a <script> tag, like < and >.
PHP$data = [ "@context" => "https:#6A9955">//schema.org", "@type" => "Product", "name" => $user_input ]; $jsonLd = json_encode($data, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
Using JSON_HEX_TAG is the secret sauce in PHP. It forces the encoder to convert < and > into their Unicode escape sequences (\u003C and \u003E). This renders the JSON-LD payload safe even if injected into a script block, because the browser won't interpret the characters as the end of a tag.
Why Context-Aware Sanitization Matters
You might wonder why we can't just use a general-purpose sanitizer. The problem is that sanitizers are designed for HTML. They strip out <iframe> or <script> tags, but they don't know how to handle the nuances of JSON syntax.
If you try to sanitize your JSON-LD with an HTML library, you might accidentally strip characters that are valid in JSON but look "suspicious" to the library. This leads to broken structured data and zero SEO benefit. Always separate your data concerns:
- Validation: Check the input format (e.g., is this a valid URL? Is this string length acceptable?).
- Serialization: Convert the data to JSON using native tools.
- Escaping: Ensure the final output is safe for the specific medium (the
<script>tag).
If you are dealing with more complex data structures, remember that JSON-Patch API security: Stop Injection and Data Corruption also requires this same level of rigor. Data corruption and injection are two sides of the same coin when you allow user input to define the structure of your payloads.
Summary of Best Practices
| Technique | Status | Why |
|---|---|---|
| String Concatenation | Dangerous | Prone to breaking out of JSON context |
| HTML Encoding | Ineffective | Breaks JSON-LD schema validity |
| JSON Serialization | Recommended | Handles escaping and syntax correctly |
JSON_HEX_TAG (PHP) | Essential | Prevents <script> tag injection |
Final Thoughts
I once spent about two days debugging an issue where our structured data was failing Google's Rich Results test. It turned out our "sanitizer" was mangling the JSON by removing quotes it deemed unnecessary. Since then, I've moved exclusively to using native serialization and hex-tag encoding.
Don't try to be clever with custom regex patterns for escaping JSON. It’s an exercise in futility that will almost certainly leave you vulnerable to a bypass. Stick to the built-in language features, keep your data structures flat where possible, and always test your output against a validator. I'm still wary of deeply nested objects coming from third-party APIs—they are often the weakest link in the chain.