Skip to content

Commit

Permalink
fix #3700: json loader preserves __proto__ keys
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Mar 14, 2024
1 parent 30bed2d commit 4780075
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 3 deletions.
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,36 @@
}();
```

* JSON loader now preserves `__proto__` properties ([#3700](https://github.com/evanw/esbuild/issues/3700))

Copying JSON source code into a JavaScript file will change its meaning if a JSON object contains the `__proto__` key. A literal `__proto__` property in a JavaScript object literal sets the prototype of the object instead of adding a property named `__proto__`, while a literal `__proto__` property in a JSON object literal just adds a property named `__proto__`. With this release, esbuild will now work around this problem by converting JSON to JavaScript with a computed property key in this case:

```js
// Original code
import data from 'data:application/json,{"__proto__":{"fail":true}}'
if (Object.getPrototypeOf(data)?.fail) throw 'fail'

// Old output (with --bundle)
(() => {
// <data:application/json,{"__proto__":{"fail":true}}>
var json_proto_fail_true_default = { __proto__: { fail: true } };

// entry.js
if (Object.getPrototypeOf(json_proto_fail_true_default)?.fail)
throw "fail";
})();

// New output (with --bundle)
(() => {
// <data:application/json,{"__proto__":{"fail":true}}>
var json_proto_fail_true_default = { ["__proto__"]: { fail: true } };

// example.mjs
if (Object.getPrototypeOf(json_proto_fail_true_default)?.fail)
throw "fail";
})();
```
* Improve dead code removal of `switch` statements ([#3659](https://github.com/evanw/esbuild/issues/3659))

With this release, esbuild will now remove `switch` statements in branches when minifying if they are known to never be evaluated:
Expand Down
4 changes: 3 additions & 1 deletion internal/bundler/bundler.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,9 @@ func parseFile(args parseArgs) {
result.ok = true

case config.LoaderJSON, config.LoaderWithTypeJSON:
expr, ok := args.caches.JSONCache.Parse(args.log, source, js_parser.JSONOptions{})
expr, ok := args.caches.JSONCache.Parse(args.log, source, js_parser.JSONOptions{
UnsupportedJSFeatures: args.options.UnsupportedJSFeatures,
})
ast := js_parser.LazyExportAST(args.log, source, js_parser.OptionsFromConfig(&args.options), expr, "")
if loader == config.LoaderWithTypeJSON {
// The exports kind defaults to "none", in which case the linker picks
Expand Down
43 changes: 43 additions & 0 deletions internal/bundler_tests/bundler_loader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1632,3 +1632,46 @@ func TestLoaderBundleWithTypeJSONOnlyDefaultExport(t *testing.T) {
`,
})
}

func TestLoaderJSONPrototype(t *testing.T) {
loader_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.js": `
import data from "./data.json"
console.log(data)
`,
"/data.json": `{
"": "The property below should be converted to a computed property:",
"__proto__": { "foo": "bar" }
}`,
},
entryPaths: []string{"/entry.js"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputFile: "/out.js",
MinifySyntax: true,
},
})
}

func TestLoaderJSONPrototypeES5(t *testing.T) {
loader_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.js": `
import data from "./data.json"
console.log(data)
`,
"/data.json": `{
"": "The property below should NOT be converted to a computed property for ES5:",
"__proto__": { "foo": "bar" }
}`,
},
entryPaths: []string{"/entry.js"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputFile: "/out.js",
MinifySyntax: true,
UnsupportedJSFeatures: es(5),
},
})
}
24 changes: 24 additions & 0 deletions internal/bundler_tests/snapshots/snapshots_loader.txt
Original file line number Diff line number Diff line change
Expand Up @@ -899,6 +899,30 @@ TestLoaderJSONNoBundleIIFE
require_test();
})();

================================================================================
TestLoaderJSONPrototype
---------- /out.js ----------
// data.json
var data_default = {
"": "The property below should be converted to a computed property:",
["__proto__"]: { foo: "bar" }
};

// entry.js
console.log(data_default);

================================================================================
TestLoaderJSONPrototypeES5
---------- /out.js ----------
// data.json
var data_default = {
"": "The property below should NOT be converted to a computed property for ES5:",
__proto__: { foo: "bar" }
};

// entry.js
console.log(data_default);

================================================================================
TestLoaderJSONSharedWithMultipleEntriesIssue413
---------- /out/a.js ----------
Expand Down
14 changes: 12 additions & 2 deletions internal/js_parser/json_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package js_parser
import (
"fmt"

"github.com/evanw/esbuild/internal/compat"
"github.com/evanw/esbuild/internal/helpers"
"github.com/evanw/esbuild/internal/js_ast"
"github.com/evanw/esbuild/internal/js_lexer"
Expand Down Expand Up @@ -142,6 +143,14 @@ func (p *jsonParser) parseExpr() js_ast.Expr {
Key: key,
ValueOrNil: value,
}

// The key "__proto__" must not be a string literal in JavaScript because
// that actually modifies the prototype of the object. This can be
// avoided by using a computed property key instead of a string literal.
if helpers.UTF16EqualsString(keyString, "__proto__") && !p.options.UnsupportedJSFeatures.Has(compat.ObjectExtensions) {
property.Flags |= js_ast.PropertyIsComputed
}

properties = append(properties, property)
}

Expand All @@ -163,8 +172,9 @@ func (p *jsonParser) parseExpr() js_ast.Expr {
}

type JSONOptions struct {
Flavor js_lexer.JSONFlavor
ErrorSuffix string
UnsupportedJSFeatures compat.JSFeature
Flavor js_lexer.JSONFlavor
ErrorSuffix string
}

func ParseJSON(log logger.Log, source logger.Source, options JSONOptions) (result js_ast.Expr, ok bool) {
Expand Down
37 changes: 37 additions & 0 deletions scripts/end-to-end-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -3528,6 +3528,43 @@ for (const minify of [[], ['--minify-syntax']]) {
`,
}),
)

// https://github.com/evanw/esbuild/issues/3700
tests.push(
test(['in.js', '--bundle', '--outfile=node.js'].concat(minify), {
'in.js': `
import imported from './data.json'
const native = JSON.parse(\`{
"hello": "world",
"__proto__": {
"sky": "universe"
}
}\`)
const literal1 = {
"hello": "world",
"__proto__": {
"sky": "universe"
}
}
const literal2 = {
"hello": "world",
["__proto__"]: {
"sky": "universe"
}
}
if (Object.getPrototypeOf(native)?.sky) throw 'fail: native'
if (!Object.getPrototypeOf(literal1)?.sky) throw 'fail: literal1'
if (Object.getPrototypeOf(literal2)?.sky) throw 'fail: literal2'
if (Object.getPrototypeOf(imported)?.sky) throw 'fail: imported'
`,
'data.json': `{
"hello": "world",
"__proto__": {
"sky": "universe"
}
}`,
}),
)
}

// Test minification of top-level symbols
Expand Down

0 comments on commit 4780075

Please sign in to comment.