I shipped a small tool page about GitHub projects as a WordPress post, with the JavaScript embedded in a Custom HTML (<!-- wp:html -->) block. Opened from disk, it worked. On the live site, every number on the page came out like this:
180,491 -> 1undefinedundefined,491
v0.33.3 -> vundefined.undefinedundefined.undefined
Some digits survived and some turned into the string undefined. That pattern is specific enough to trace back to one character.
The helper that broke
The page had the usual HTML-escape helper:
function esc(s) {
return String(s).replace(/[&<>"']/g, c => MAP[c]);
}
On the live page, the source of that line read:
return String(s).replace(/[&<>"']/g, c => MAP[c]);
WordPress's wptexturize filter ran over the post content, found a lone &, and turned it into the numeric entity &. The Custom HTML block didn't protect the script, and being inside a <script> tag didn't either.
Inside a regex character class, & isn't one character. It's six, so the class now also matched #, 0, 3, 8 and ;. Every 0, 3 and 8 in my numbers got matched, looked up in MAP, found nothing, and was replaced with undefined. That's why 180,491 kept its 1, 4, 9 and 1 and lost the 8 and the 0.
What it didn't touch
The same script had nine && operators, and all nine came through unchanged. It's specifically a single, free-standing & that gets converted. That's also why the rest of the page logic ran fine and the only symptom was in the output of one helper, which made it look like a data problem rather than a broken script.
The fix
Don't write a bare & in JavaScript that WordPress will filter. In a regex or a string literal, the Unicode escape means the same thing to the JS engine and gives the filter nothing to rewrite:
return String(s).replace(/[\u0026<>"']/g, c => MAP[c]);
The other route is to keep the script out of post content entirely, in a separate .js file, since the content filters only run on the post body.
Why I didn't catch it before publishing
The bug can't exist locally. The file I tested from file:// was exactly what I wrote; the & only appears after WordPress renders the post. My local check was honest and also testing a different file from the one visitors get.
The check I run now after publishing anything with inline JS is to fetch the live HTML and grep the script region for &. It takes a few seconds, and it's the only test that looks at what the browser actually receives.
Has anyone found a WordPress content filter that touches something other than & inside embedded scripts? I'd like to know what else belongs in that grep.
Top comments (0)