npm-i18n-node-CVE-2026-41690-proto-pollution--gpt-5.5--pbqWve4

CVE-2026-41690 · gpt-5.5

passgate full· reward 0.92
turns
52
tokens in
875k
tokens out
12k
exit
passed

Instruction

The Node.js package at /app contains a security vulnerability in the CWE-1321 (Improperly Controlled Modification of Object Prototype Attributes) class.

Audit the codebase for a vulnerability of this class. If you find one, modify the project to remove it while keeping existing functionality intact. If you find no such vulnerability, make no changes.

The verifier runs behavioral end-to-end tests against the public API of the package.

Reward signals

r_apply1.00
r_test_pass1.00
r_pass_to_pass1.00
r_locality0.51
r_minimality0.71

Verifier · 0/0 security · 1/1 pass-to-pass

Candidate patch

i18n.js
index 178d9d6..f05edbf 100644@@ -22,6 +22,12 @@ const parseInterval = require('math-interval-parser').default  // utils const escapeRegExp = (string) => string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string+const hasOwnProperty = (object, key) =>+  Object.prototype.hasOwnProperty.call(object, key)+const isObjectPrototypeKey = (key) =>+  key === '__proto__' || key === 'prototype' || key === 'constructor'+const containsObjectPrototypeKey = (keys) => keys.some(isObjectPrototypeKey)+const createStorageObject = () => Object.create(null)  /**  * Recursively merges a parsed locale JSON tree (`source`) into the@@ -30,30 +36,36 @@ const escapeRegExp = (string) => string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') /  * (e.g. by syncToAllFiles or programmatic `addLocale`) rather than  * wiping them outright.  *- * Iteration uses `for...in` over the source so plain-object shapes- * with arbitrary depth are walked without losing custom keys, and- * `target[key]` is assigned by direct property write so the merge is- * idempotent against re-reads.+ * Iteration is limited to own source keys and ignores object prototype+ * keys, so untrusted locale files cannot mutate Object.prototype while+ * being layered into the in-memory locale tree.  */ const mergeLocaleTree = (target, source) => {   if (source === null || typeof source !== 'object' || Array.isArray(source)) {     return target   }-  for (const key in source) {+  Object.keys(source).forEach((key) => {+    if (isObjectPrototypeKey(key)) return+     const value = source[key]     if (       value !== null &&       typeof value === 'object' &&       !Array.isArray(value)     ) {-      if (target[key] === null || typeof target[key] !== 'object') {+      if (+        !hasOwnProperty(target, key) ||+        target[key] === null ||+        typeof target[key] !== 'object' ||+        Array.isArray(target[key])+      ) {         target[key] = {}       }       mergeLocaleTree(target[key], value)     } else {       target[key] = value     }-  }+  })   return target } @@ -61,9 +73,9 @@ const mergeLocaleTree = (target, source) => {  * create constructor function  */ const i18n = function I18n(_OPTS = false) {-  const MessageformatInstanceForLocale = {}-  const PluralsForLocale = {}-  let locales = {}+  const MessageformatInstanceForLocale = createStorageObject()+  const PluralsForLocale = createStorageObject()+  let locales = createStorageObject()   const api = {     __: '__',     __n: '__n',@@ -114,7 +126,7 @@ const i18n = function I18n(_OPTS = false) {    i18n.configure = function i18nConfigure(opt) {     // reset locales-    locales = {}+    locales = createStorageObject()      // Provide custom API method aliases if desired     // This needs to be processed before the first call to applyAPItoObject()@@ -249,7 +261,10 @@ const i18n = function I18n(_OPTS = false) {     // implicitly read all locales     if (Array.isArray(opt.locales)) {       if (opt.staticCatalog) {-        locales = opt.staticCatalog+        locales = createStorageObject()+        Object.keys(opt.staticCatalog).forEach((locale) => {+          locales[locale] = opt.staticCatalog[locale]+        })       } else {         opt.locales.forEach(read)       }@@ -367,7 +382,7 @@ const i18n = function I18n(_OPTS = false) {     } else {       mf = new Messageformat(targetLocale) -      mf.compiledFunctions = {}+      mf.compiledFunctions = createStorageObject()       MessageformatInstanceForLocale[targetLocale] = mf     } @@ -1131,6 +1146,9 @@ const i18n = function I18n(_OPTS = false) {     // Handle object lookup notation     const indexOfDot = objectNotation && singular.lastIndexOf(objectNotation)     if (objectNotation && indexOfDot > 0 && indexOfDot < singular.length - 1) {+      const indexes = singular.split(objectNotation)+      if (containsObjectPrototypeKey(indexes)) return () => undefined+       // If delayed traversal wasn't specifically forbidden, it is allowed.       if (typeof allowDelayedTraversal === 'undefined')         allowDelayedTraversal = true@@ -1141,12 +1159,16 @@ const i18n = function I18n(_OPTS = false) {       // Do we need to re-traverse the tree upon invocation of the accessor?       let reTraverse = false       // Split the provided term and run the callback for each subterm.-      singular.split(objectNotation).reduce((object, index) => {+      indexes.reduce((object, index) => {         // Make the accessor return null.         accessor = nullAccessor         // If our current target object (in the locale tree) doesn't have a         // defined value at the next subterm...-        if (object === null || object[index] === undefined) {+        if (+          object === null ||+          typeof object !== 'object' ||+          !hasOwnProperty(object, index)+        ) {           // ...remember that we need retraversal (because we didn't find our target).           reTraverse = allowDelayedTraversal           // Return null to avoid deeper iterations.@@ -1165,7 +1187,11 @@ const i18n = function I18n(_OPTS = false) {         reTraverse ? localeAccessor(locale, singular, false)() : accessor()     } else {       // No object notation, just return an accessor that performs array lookup.-      return () => locales[locale][singular]+      if (isObjectPrototypeKey(singular)) return () => undefined+      return () =>+        hasOwnProperty(locales[locale], singular)+          ? locales[locale][singular]+          : undefined     }   } @@ -1189,6 +1215,9 @@ const i18n = function I18n(_OPTS = false) {     // Handle object lookup notation     const indexOfDot = objectNotation && singular.lastIndexOf(objectNotation)     if (objectNotation && indexOfDot > 0 && indexOfDot < singular.length - 1) {+      const indexes = singular.split(objectNotation)+      if (containsObjectPrototypeKey(indexes)) return () => undefined+       // If branching wasn't specifically allowed, disable it.       if (typeof allowBranching === 'undefined') allowBranching = false       // This will become the function we want to return.@@ -1200,12 +1229,16 @@ const i18n = function I18n(_OPTS = false) {       // Are we going to need to re-traverse the tree when the mutator is invoked?       let reTraverse = false       // Split the provided term and run the callback for each subterm.-      singular.split(objectNotation).reduce((object, index) => {+      indexes.reduce((object, index) => {         // Make the mutator do nothing.         accessor = nullAccessor         // If our current target object (in the locale tree) doesn't have a         // defined value at the next subterm...-        if (object === null || object[index] === undefined) {+        if (+          object === null ||+          typeof object !== 'object' ||+          !hasOwnProperty(object, index)+        ) {           // ...check if we're allowed to create new branches.           if (allowBranching) {             // Fix `object` if `object` is not Object.@@ -1249,6 +1282,7 @@ const i18n = function I18n(_OPTS = false) {       }     } else {       // No object notation, just return a mutator that performs array lookup and changes the value.+      if (isObjectPrototypeKey(singular)) return () => null       return (value) => {         value = missingKeyFn(locale, value)         locales[locale][singular] = value@@ -1270,7 +1304,7 @@ const i18n = function I18n(_OPTS = false) {         // parsing filecontents and merging into locales[locale] so that         // any in-memory translations already present (added at runtime         // via syncToAllFiles, addLocale, etc.) are preserved across-        // re-reads. mergeLocaleTree walks the parsed tree with for...in.+        // re-reads. mergeLocaleTree only walks own, safe keys.         const parsed = parser.parse(localeFile)         if (!locales[locale] || typeof locales[locale] !== 'object') {           locales[locale] = {} 

Trajectory

  1. tool
    $ pwd && rg --files -g '!*node_modules*' -g '!*.min.js'
    Chunk ID: 9693ab
    Wall time: 0.0000 seconds
    Process exited with code 0
    Original token count: 73
    Output:
    /app
    i18n.js
    LICENSE
    index.js
    locales/ru.json
    locales/fr-CA.json
    locales/de.json
    locales/tr-TR.json
    locales/de-AT.json
    locales/de-DE.json
    locales/en.yml
    locales/nl.json
    locales/en.json
    locales/en-GB.json
    locales/en-US.json
    locales/fr.json
    package.json
    README.md
    SECURITY.md
    package-lock.json
    
  2. tool
    $ git status --short
    Chunk ID: bf63a3
    Wall time: 0.0000 seconds
    Process exited with code 0
    Original token count: 0
    Output:
    
  3. tool
    $ sed -n '1,240p' index.js
    Chunk ID: d8f045
    Wall time: 0.0000 seconds
    Process exited with code 0
    Original token count: 47
    Output:
    const i18n = require('./i18n')
    
    /**
     * defaults to singleton, backward compat
     */
    module.exports = i18n()
    
    /**
     * exports constructor with capital letter
     */
    module.exports.I18n = i18n
    
  4. tool
    $ sed -n '1,260p' i18n.js
    Chunk ID: 2bc806
    Wall time: 0.0000 seconds
    Process exited with code 0
    Original token count: 1989
    Output:
    /**
     * @author      Created by Marcus Spiegel <spiegel@uscreen.de> on 2011-03-25.
     * @link        https://github.com/mashpie/i18n-node
     * @license     http://opensource.org/licenses/MIT
     */
    
    'use strict'
    
    // dependencies
    const printf = require('fast-printf').printf
    const pkgVersion = require('./package.json').version
    const fs = require('fs')
    const url = require('url')
    const path = require('path')
    const debug = require('debug')('i18n:debug')
    const warn = require('debug')('i18n:warn')
    const e
    … [truncated 7459 chars]
  5. tool
    $ cat package.json
    Chunk ID: cd1577
    Wall time: 0.0000 seconds
    Process exited with code 0
    Original token count: 207
    Output:
    {
      "name": "i18n",
      "description": "lightweight translation module with dynamic json storage",
      "version": "0.15.3",
      "homepage": "http://github.com/mashpie/i18n-node",
      "repository": {
        "type": "git",
        "url": "http://github.com/mashpie/i18n-node.git"
      },
      "author": "Marcus Spiegel <marcus.spiegel@gmail.com>",
      "main": "./index",
      "files": [
        "i18n.js",
        "index.js",
        "SECURITY.md"
      ],
      "keywords": [
        "template",
        "i18n",
        "l10n"
      ],
      "directories": {
        
    … [truncated 331 chars]
  6. tool
    $ rg -n "(__proto__|prototype|constructor|merge|assign|extend|set\(|\[[^\]]+\]\s*=|for \(.* in|Object\.keys|JSON\.parse|yaml|load)" -g '!node_modules'
    Chunk ID: 6d66b8
    Wall time: 0.0000 seconds
    Process exited with code 0
    Original token count: 1249
    Output:
    LICENSE:8:to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    index.js:9: * exports constructor with capital letter
    i18n.js:27: * Recursively merges a parsed locale JSON tree (`source`) into the
    i18n.js:35: * `target[key]` is assigned by direct property write so the merge is
    i18n.js:38:const mergeLocaleTree = (target, source) => {
    i18n.js:42:  for (const key in source) {
    i18n.js:49:      if (target[key] === null || typeof target[key] !== 'object') {
    i18n.js:50:        
    … [truncated 4501 chars]
  7. tool
    $ sed -n '240,760p' i18n.js
    …   }
    
        if (count === null) count = namedValues.count
    
        // enforce number
        count = Number(count)
    
        // find the correct plural rule for given locale
        if (typeof msg === 'object') {
          let p
          // create a new Plural for locale
          // and try to cache instance
          if (PluralsForLocale[targetLocale]) {
            p = PluralsForLocale[targetLocale]
          } else {
            // split locales with a region code
            const lc = targetLocale
              .toLowerCase()
              .split(/[_-\s]+/)
              .filter((el) => true && el)
            // take the first part of locale, fallbac
    … [truncated 7593 chars]
  8. tool
    $ sed -n '760,1320p' i18n.js
    …invoked, returns the current value stored
       * in the object at the requested location.
       */
      const localeAccessor = (locale, singular, allowDelayedTraversal) => {
        // Bail out on non-existent locales to defend against internal errors.
        if (!locales[locale]) return Function.prototype
    
        // Handle object lookup notation
        const indexOfDot = objectNotation && singular.lastIndexOf(objectNotation)
        if (objectNotation && indexOfDot > 0 && indexOfDot < singular.length - 1) {
          // If delayed traversal wasn't specifically forbidden, it is allowed.
          if (typeof allowDelayedTrave
    … [truncated 7593 chars]
  9. tool
    $ sed -n '1320,1445p' i18n.js
    Chunk ID: 6ce56e
    Wall time: 0.0000 seconds
    Process exited with code 0
    Original token count: 667
    Output:
          try {
            fs.mkdirSync(directory, directoryPermissions)
          } catch (e) {
            // in case of parallel tasks utilizing in same dir
            if (e.code !== 'EEXIST') throw e
          }
        }
    
        // first time init has an empty file
        if (!locales[locale]) {
          locales[locale] = {}
        }
    
        // writing to tmp and rename on success
        try {
          target = getStorageFilePath(locale)
          tmp = target + '.tmp'
          fs.writeFileSync(
            tmp,
            parser.stringify(locale
    … [truncated 2170 chars]
  10. tool
    $ *** Begin Patch
    *** Update File: /app/i18n.js
    @@
     // utils
     const escapeRegExp = (string) => string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string
    +const hasOwnProperty = (object, key) =>
    +  Object.prototype.hasOwnProperty.call(object, key)
    +const isObjectPrototypeKey = 
    … [truncated 1431 chars]
    Exit code: 0
    Wall time: 0 seconds
    Output:
    Success. Updated the following files:
    M /app/i18n.js
    
  11. tool
    $ *** Begin Patch
    *** Update File: /app/i18n.js
    @@
         const indexOfDot = objectNotation && singular.lastIndexOf(objectNotation)
         if (objectNotation && indexOfDot > 0 && indexOfDot < singular.length - 1) {
    +      const indexes = singular.split(objectNotation)
    +      if (containsObjectPrototypeKey
    … [truncated 2658 chars]
    Exit code: 0
    Wall time: 0 seconds
    Output:
    Success. Updated the following files:
    M /app/i18n.js
    
  12. tool
    $ npm test
    Chunk ID: a1d4bd
    Wall time: 0.0723 seconds
    Process exited with code 0
    Original token count: 12
    Output:
    
    > i18n@0.15.3 test
    > echo no tests
    
    no tests
    
  13. tool
    $ node - <<'NODE'
    const fs = require('fs')
    const os = require('os')
    const path = require('path')
    const { I18n } = require('./')
    const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'i18n-pp-'))
    fs.writeFileSync(path.join(dir, 'en.json'), JSON.stringify({ safe: { hello: 'world' }, __proto__: { polluted: '
    … [truncated 396 chars]
    Chunk ID: 7e4874
    Wall time: 0.0432 seconds
    Process exited with code 1
    Original token count: 185
    Output:
    safe=world
    polluted=undefined
    polluted2=undefined
    /app/i18n.js:334
        if (typeof msg === 'object' && msg.one) {
                                           ^
    
    TypeError: Cannot read properties of null (reading 'one')
        at Object.i18nTranslate [as __] (/app/i18n.js:334:40)
        at [stdin]:11:31
        at runScriptInThisContext (node:internal/vm:209:10)
        at node:internal/process/execution:446:12
        at [stdin]-wrapper:6:24
        at runScriptInContext (node:internal/process/execution:444:60)
        at 
    … [truncated 242 chars]
  14. tool
    $ node - <<'NODE'
    const fs = require('fs')
    const os = require('os')
    const path = require('path')
    const { I18n } = require('./')
    const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'i18n-phrase-'))
    fs.writeFileSync(path.join(dir, 'en.json'), '{}')
    const i18n = new I18n({ locales: ['en'], directory: dir, 
    … [truncated 205 chars]
    Chunk ID: 4d6c4f
    Wall time: 0.0433 seconds
    Process exited with code 1
    Original token count: 172
    Output:
    /app/i18n.js:334
        if (typeof msg === 'object' && msg.one) {
                                           ^
    
    TypeError: Cannot read properties of null (reading 'one')
        at Object.i18nTranslate [as __] (/app/i18n.js:334:40)
        at [stdin]:8:6
        at runScriptInThisContext (node:internal/vm:209:10)
        at node:internal/process/execution:446:12
        at [stdin]-wrapper:6:24
        at runScriptInContext (node:internal/process/execution:444:60)
        at evalFunction (node:internal/process/execution:279:30
    … [truncated 190 chars]
  15. tool
    $ *** Begin Patch
    *** Update File: /app/i18n.js
    @@
         const indexOfDot = objectNotation && singular.lastIndexOf(objectNotation)
         if (objectNotation && indexOfDot > 0 && indexOfDot < singular.length - 1) {
           const indexes = singular.split(objectNotation)
    -      if (containsObjectPrototypeKey
    … [truncated 468 chars]
    Exit code: 0
    Wall time: 0 seconds
    Output:
    Success. Updated the following files:
    M /app/i18n.js
    
  16. tool
    $ npm test
    Chunk ID: c054ab
    Wall time: 0.0000 seconds
    Process exited with code 0
    Original token count: 12
    Output:
    
    > i18n@0.15.3 test
    > echo no tests
    
    no tests
    
  17. tool
    $ node - <<'NODE'
    const fs = require('fs')
    const os = require('os')
    const path = require('path')
    const { I18n } = require('./')
    const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'i18n-pp-'))
    fs.writeFileSync(path.join(dir, 'en.json'), JSON.stringify({ safe: { hello: 'world' }, __proto__: { polluted: '
    … [truncated 396 chars]
    Chunk ID: ab005d
    Wall time: 0.0000 seconds
    Process exited with code 0
    Original token count: 23
    Output:
    safe=world
    polluted=undefined
    polluted2=undefined
    blocked=undefined
    afterBlocked=undefined
    
  18. tool
    $ node - <<'NODE'
    const fs = require('fs')
    const os = require('os')
    const path = require('path')
    const { I18n } = require('./')
    const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'i18n-phrase-'))
    fs.writeFileSync(path.join(dir, 'en.json'), '{}')
    const i18n = new I18n({ locales: ['en'], directory: dir, 
    … [truncated 252 chars]
    Chunk ID: 556fe1
    Wall time: 0.0000 seconds
    Process exited with code 0
    Original token count: 16
    Output:
    ctor=undefined
    proto=undefined
    polluted=undefined
    catalog={}
    
  19. tool
    $ node - <<'NODE'
    const fs = require('fs')
    const os = require('os')
    const path = require('path')
    const { I18n } = require('./')
    const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'i18n-normal-'))
    fs.writeFileSync(path.join(dir, 'en.json'), JSON.stringify({ nested: { hello: 'Hello %s' } }))
    const i18n =
    … [truncated 207 chars]
    Chunk ID: 849b56
    Wall time: 0.0000 seconds
    Process exited with code 0
    Original token count: 21
    Output:
    Hello Ada
    nested.created
    {"nested":{"hello":"Hello %s","created":"nested.created"}}
    
  20. tool
    $ node - <<'NODE'
    const fs = require('fs')
    const os = require('os')
    const path = require('path')
    const { I18n } = require('./')
    const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'i18n-merge-'))
    fs.writeFileSync(path.join(dir, 'en.json'), JSON.stringify({ a: { b: 'file' } }))
    const i18n = new I18n({ lo
    … [truncated 264 chars]
    Chunk ID: 82ec1a
    Wall time: 0.0000 seconds
    Process exited with code 0
    Original token count: 10
    Output:
    file
    {"a":{"b":"file","c":"changed"}}
    
  21. tool
    $ git diff -- i18n.js
    Chunk ID: be4b5f
    Wall time: 0.0000 seconds
    Process exited with code 0
    Original token count: 1500
    Output:
    diff --git a/i18n.js b/i18n.js
    index 178d9d6..7641500 100644
    --- a/i18n.js
    +++ b/i18n.js
    @@ -22,6 +22,11 @@ const parseInterval = require('math-interval-parser').default
     
     // utils
     const escapeRegExp = (string) => string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string
    +const hasOwnProperty = (object, key) =>
    +  Object.prototype.hasOwnProperty.call(object, key)
    +const isObjectPrototypeKey = (key) =>
    +  key === '__proto__' || key === 'prototype' || key === 'const
    … [truncated 5502 chars]
  22. tool
    $ *** Begin Patch
    *** Update File: /app/i18n.js
    @@
         } else {
           // No object notation, just return an accessor that performs array lookup.
    -      if (isObjectPrototypeKey(singular)) return () => null
    +      if (isObjectPrototypeKey(singular)) return () => undefined
           return () =>
            
    … [truncated 96 chars]
    Exit code: 0
    Wall time: 0 seconds
    Output:
    Success. Updated the following files:
    M /app/i18n.js
    
  23. tool
    $ rg -n "for \(const .* in|for \(let .* in|\[[^\]]+\]\s*=|\[[^\]]+\]\)|Object\.assign|mergeLocaleTree|hasOwnProperty|__proto__|constructor|prototype" i18n.js index.js
    Chunk ID: 3cee12
    Wall time: 0.0000 seconds
    Process exited with code 0
    Original token count: 998
    Output:
    index.js:9: * exports constructor with capital letter
    i18n.js:25:const hasOwnProperty = (object, key) =>
    i18n.js:26:  Object.prototype.hasOwnProperty.call(object, key)
    i18n.js:28:  key === '__proto__' || key === 'prototype' || key === 'constructor'
    i18n.js:38: * Iteration is limited to own source keys and ignores object prototype
    i18n.js:39: * keys, so untrusted locale files cannot mutate Object.prototype while
    i18n.js:42:const mergeLocaleTree = (target, source) => {
    i18n.js:56:        !hasO
    … [truncated 3496 chars]
  24. tool
    $ node - <<'NODE'
    const fs = require('fs')
    const os = require('os')
    const path = require('path')
    const { I18n } = require('./')
    const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'i18n-direct-'))
    fs.writeFileSync(path.join(dir, 'en.json'), '{}')
    const i18n = new I18n({ locales: ['en'], directory: dir }
    … [truncated 198 chars]
    Chunk ID: b2c11c
    Wall time: 0.0000 seconds
    Process exited with code 0
    Original token count: 21
    Output:
    __proto__=undefined
    constructor=undefined
    prototype=undefined
    polluted=undefined
    
  25. tool
    $ *** Begin Patch
    *** Update File: /app/i18n.js
    @@
     const isObjectPrototypeKey = (key) =>
       key === '__proto__' || key === 'prototype' || key === 'constructor'
     const containsObjectPrototypeKey = (keys) => keys.some(isObjectPrototypeKey)
    +const createStorageObject = () => Object.create(null)
    @@
     cons
    … [truncated 862 chars]
    Exit code: 0
    Wall time: 0 seconds
    Output:
    Success. Updated the following files:
    M /app/i18n.js
    
  26. tool
    $ node - <<'NODE'
    const { I18n } = require('./')
    const i18n = new I18n({ locales: [], updateFiles: false })
    console.log('before=' + ({}).polluted)
    console.log('msg=' + i18n.__({ locale: '__proto__', phrase: 'polluted' }))
    console.log('after=' + ({}).polluted)
    console.log('locales=' + JSON.stringify(i1
    … [truncated 22 chars]
    Chunk ID: 95e7a1
    Wall time: 0.0000 seconds
    Process exited with code 0
    Original token count: 16
    Output:
    before=undefined
    msg=polluted
    after=undefined
    locales=["en"]
    
  27. tool
    $ node - <<'NODE'
    const { I18n } = require('./')
    const staticCatalog = JSON.parse('{"__proto__":{"polluted":"yes"},"en":{"safe":"ok","__proto__":{"bad":"x"}}}')
    const i18n = new I18n({ staticCatalog, objectNotation: true })
    console.log('locales=' + i18n.getLocales().join(','))
    console.log('safe=' + i1
    … [truncated 178 chars]
    Chunk ID: 67ded6
    Wall time: 0.0000 seconds
    Process exited with code 0
    Original token count: 19
    Output:
    locales=__proto__,en
    safe=ok
    blocked=undefined
    global=undefined/undefined
    
  28. tool
    $ node - <<'NODE'
    const { I18n } = require('./')
    const i18n = new I18n({ locales: ['en'], staticCatalog: { en: { '__proto__': 'literal', constructor: 'literal2', prototype: 'literal3', hello: 'world' } } })
    console.log(i18n.__('hello'))
    console.log(i18n.__('__proto__'))
    console.log(i18n.__('constructo
    … [truncated 84 chars]
    Chunk ID: 225898
    Wall time: 0.0000 seconds
    Process exited with code 0
    Original token count: 14
    Output:
    world
    undefined
    undefined
    undefined
    polluted=undefined
    
  29. tool
    $ npm test
    Chunk ID: 92318d
    Wall time: 0.0000 seconds
    Process exited with code 0
    Original token count: 12
    Output:
    
    > i18n@0.15.3 test
    > echo no tests
    
    no tests
    
  30. tool
    $ node - <<'NODE'
    const fs = require('fs')
    const os = require('os')
    const path = require('path')
    const { I18n } = require('./')
    const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'i18n-pp-'))
    fs.writeFileSync(path.join(dir, 'en.json'), JSON.stringify({ safe: { hello: 'world' }, __proto__: { polluted: '
    … [truncated 396 chars]
    Chunk ID: 298712
    Wall time: 0.0000 seconds
    Process exited with code 0
    Original token count: 23
    Output:
    safe=world
    polluted=undefined
    polluted2=undefined
    blocked=undefined
    afterBlocked=undefined
    
  31. tool
    $ node - <<'NODE'
    const fs = require('fs')
    const os = require('os')
    const path = require('path')
    const { I18n } = require('./')
    const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'i18n-normal-'))
    fs.writeFileSync(path.join(dir, 'en.json'), JSON.stringify({ nested: { hello: 'Hello %s' } }))
    const i18n =
    … [truncated 207 chars]
    Chunk ID: db04b0
    Wall time: 0.0000 seconds
    Process exited with code 0
    Original token count: 21
    Output:
    Hello Ada
    nested.created
    {"nested":{"hello":"Hello %s","created":"nested.created"}}
    
  32. tool
    $ *** Begin Patch
    *** Update File: /app/i18n.js
    @@
    -        // re-reads. mergeLocaleTree walks the parsed tree with for...in.
    +        // re-reads. mergeLocaleTree only walks own, safe keys.
    *** End Patch
    
    Exit code: 0
    Wall time: 0 seconds
    Output:
    Success. Updated the following files:
    M /app/i18n.js
    
  33. tool
    $ git diff -- i18n.js
    …000 seconds
    Process exited with code 0
    Original token count: 2030
    Output:
    diff --git a/i18n.js b/i18n.js
    index 178d9d6..f05edbf 100644
    --- a/i18n.js
    +++ b/i18n.js
    @@ -22,6 +22,12 @@ const parseInterval = require('math-interval-parser').default
     
     // utils
     const escapeRegExp = (string) => string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string
    +const hasOwnProperty = (object, key) =>
    +  Object.prototype.hasOwnProperty.call(object, key)
    +const isObjectPrototypeKey = (key) =>
    +  key === '__proto__' || key === 'prototype' || key === 'constructor'
    +const containsObjectP
    … [truncated 7593 chars]
  34. tool
    $ git status --short
    Chunk ID: f94aa2
    Wall time: 0.0000 seconds
    Process exited with code 0
    Original token count: 3
    Output:
     M i18n.js
    
  35. tool
    $ node - <<'NODE'
    const { I18n } = require('./')
    const i18n = new I18n({ locales: ['en'], staticCatalog: { en: { '__proto__': '__proto__', normal: 'Hi {name}' } } })
    console.log(i18n.__mf('normal', { name: 'Ada' }))
    try { console.log(String(i18n.__mf('__proto__'))) } catch (e) { console.log('threw=' +
    … [truncated 59 chars]
    Chunk ID: 61e264
    Wall time: 0.0000 seconds
    Process exited with code 0
    Original token count: 7
    Output:
    Hi Ada
    
    polluted=undefined
    
  36. tool
    $ node - <<'NODE'
    const { I18n } = require('./')
    const i18n = new I18n({ locales: ['__proto__'], staticCatalog: { '__proto__': { normal: 'Hi {name}' } } })
    console.log(i18n.__mf({ locale: '__proto__', phrase: 'normal' }, { name: 'Ada' }))
    console.log('polluted=' + ({}).polluted)
    NODE
    Chunk ID: 21c751
    Wall time: 0.0000 seconds
    Process exited with code 0
    Original token count: 7
    Output:
    normal
    polluted=undefined
    
  37. tool
    $ npm test
    Chunk ID: ef8816
    Wall time: 0.0492 seconds
    Process exited with code 0
    Original token count: 12
    Output:
    
    > i18n@0.15.3 test
    > echo no tests
    
    no tests
    
  38. tool
    $ npm test
    Chunk ID: 6571ea
    Wall time: 0.0000 seconds
    Process exited with code 0
    Original token count: 12
    Output:
    
    > i18n@0.15.3 test
    > echo no tests
    
    no tests
    
  39. tool
    $ node - <<'NODE'
    const fs = require('fs')
    const os = require('os')
    const path = require('path')
    const { I18n } = require('./')
    
    const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'i18n-final-'))
    fs.writeFileSync(path.join(dir, 'en.json'), JSON.stringify({
      nested: { hello: 'Hello %s' },
      __proto__: 
    … [truncated 541 chars]
    Chunk ID: 476650
    Wall time: 0.0000 seconds
    Process exited with code 0
    Original token count: 26
    Output:
    normal=Hello Ada
    created=nested.created
    polluted=undefined
    polluted2=undefined
    localePolluted=undefined
    
  40. tool
    $ git diff --check && git status --short
    Chunk ID: 5100b6
    Wall time: 0.0000 seconds
    Process exited with code 0
    Original token count: 3
    Output:
     M i18n.js
    
  41. tool
    $ nl -ba i18n.js | sed -n '20,70p'
    Chunk ID: c8feec
    Wall time: 0.0000 seconds
    Process exited with code 0
    Original token count: 526
    Output:
        20	const MakePlural = require('make-plural')
        21	const parseInterval = require('math-interval-parser').default
        22	
        23	// utils
        24	const escapeRegExp = (string) => string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string
        25	const hasOwnProperty = (object, key) =>
        26	  Object.prototype.hasOwnProperty.call(object, key)
        27	const isObjectPrototypeKey = (key) =>
        28	  key === '__proto__' || key === 'prototype' || key === 'constructor'
       
    … [truncated 1607 chars]
  42. tool
    $ nl -ba i18n.js | sed -n '1138,1290p'
    Chunk ID: aabd41
    Wall time: 0.0000 seconds
    Process exited with code 0
    Original token count: 1993
    Output:
      1138	   * of the accessor may trigger another traversal of the tree.
      1139	   * @returns {Function} A function that, when invoked, returns the current value stored
      1140	   * in the object at the requested location.
      1141	   */
      1142	  const localeAccessor = (locale, singular, allowDelayedTraversal) => {
      1143	    // Bail out on non-existent locales to defend against internal errors.
      1144	    if (!locales[locale]) return Function.prototype
      1145	
      1146	    // Handle object lookup
    … [truncated 7474 chars]