Rules Hub
Coding Rules Library
Rule priority, scope & exceptions
Use this to align rules with the senior-level structure (P0/P1/P2, scope, exceptions/tradeoffs).
Reject unknown keys and never deep-merge untrusted JSON (prototype pollution)
Make schemas reject unknown keys rather than silently strip them, and never recursively merge/assign parsed JSON onto an object — both let attackers smuggle fields, including prototype-pollution payloads.
Bad example
| 1 | // 1) Schema silently drops unknown keys -> over-posting goes unnoticed |
| 2 | const Dto = z.object({ name: z.string() }); // default: extra keys stripped, no error |
| 3 |
|
| 4 | // 2) Deep-merging untrusted JSON enables prototype pollution |
| 5 | function deepMerge(target: any, src: any) { |
| 6 | for (const key of Object.keys(src)) { |
| 7 | if (typeof src[key] === 'object' && src[key] !== null) { |
| 8 | target[key] = target[key] ?? {}; |
| 9 | deepMerge(target[key], src[key]); // src = JSON.parse('{"__proto__":{"isAdmin":true}}') |
| 10 | } else { |
| 11 | target[key] = src[key]; |
| 12 | } |
| 13 | } |
| 14 | return target; |
| 15 | } |
| 16 | const config = deepMerge({}, JSON.parse(req.body.settings)); |
Explanation (EN)
A non-strict schema accepts and discards unexpected fields, so neither over-posting attempts nor a drifting client contract ever surface. A naive recursive merge that walks `__proto__`/`constructor`/`prototype` keys mutates `Object.prototype`, so suddenly every object in the process appears to have `isAdmin: true` — classic prototype pollution (OWASP A08, Insecure Deserialization).
Objašnjenje (HR)
Ne-stroga shema prihvati i odbaci neočekivana polja, pa se ni pokušaji over-postinga ni razilaženje klijentskog ugovora nikad ne pokažu. Naivni rekurzivni merge koji prolazi kroz `__proto__`/`constructor`/`prototype` ključeve mutira `Object.prototype`, pa odjednom svaki objekt u procesu izgleda kao da ima `isAdmin: true` — klasična prototype pollution (OWASP A08, nesigurna deserijalizacija).
Good example
| 1 | // 1) Strict schema: unknown keys are a 400, not a silent drop |
| 2 | const Dto = z |
| 3 | .object({ name: z.string().min(1), theme: z.enum(['light', 'dark']) }) |
| 4 | .strict(); |
| 5 |
|
| 6 | // 2) Build a typed object from validated fields instead of merging raw JSON |
| 7 | function toSettings(parsed: z.infer<typeof Dto>) { |
| 8 | return { name: parsed.name, theme: parsed.theme }; // no dynamic keys, no recursion |
| 9 | } |
| 10 |
|
| 11 | const result = Dto.safeParse(JSON.parse(req.body.settings)); |
| 12 | if (!result.success) return res.status(400).json({ error: result.error.flatten() }); |
| 13 | const settings = toSettings(result.data); |
| 14 | // If a merge is truly required, use a guarded one and never copy __proto__/constructor/prototype keys. |
Explanation (EN)
`.strict()` turns unexpected keys into explicit errors, exposing over-posting and contract drift. Constructing the result object from named, validated fields means no attacker-controlled key ever becomes a property name and no recursion ever touches `__proto__`. When a merge is unavoidable, use `Object.create(null)` targets and explicitly reject the dangerous keys.
Objašnjenje (HR)
`.strict()` pretvara neočekivane ključeve u eksplicitne greške, otkrivajući over-posting i razilaženje ugovora. Gradnja rezultata iz imenovanih, validiranih polja znači da nijedan ključ pod kontrolom napadača nikad ne postane ime svojstva i da nijedna rekurzija ne dotakne `__proto__`. Kad je merge neizbježan, koristi `Object.create(null)` ciljeve i eksplicitno odbij opasne ključeve.
Notes (EN)
Maps to OWASP A08:2021 Software & Data Integrity (Insecure Deserialization) and the Prototype Pollution cheat sheet. Zod `.strict()`, Joi `{ stripUnknown:false }`/`.unknown(false)`, and class-validator `forbidNonWhitelisted:true` all express this.
Bilješke (HR)
Mapira se na OWASP A08:2021 Software & Data Integrity (nesigurna deserijalizacija) i Prototype Pollution cheat sheet. Zod `.strict()`, Joi `{ stripUnknown:false }`/`.unknown(false)` i class-validator `forbidNonWhitelisted:true` to izražavaju.
Exceptions / Tradeoffs (EN)
Genuinely open-ended maps (e.g. user-defined metadata) can use a passthrough/record schema, but constrain the value type and key format and still strip/reject `__proto__`, `constructor`, and `prototype`.
Iznimke / Tradeoffi (HR)
Istinski otvorene mape (npr. korisnički definirani metapodaci) mogu koristiti passthrough/record shemu, ali ograniči tip vrijednosti i format ključa te i dalje odbij `__proto__`, `constructor` i `prototype`.