Skip to content

Commit 4780075

Browse files
committed
fix #3700: json loader preserves __proto__ keys
1 parent 30bed2d commit 4780075

File tree

6 files changed

+149
-3
lines changed

6 files changed

+149
-3
lines changed

CHANGELOG.md

+30
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,36 @@
3232
}();
3333
```
3434

35+
* JSON loader now preserves `__proto__` properties ([#3700](https://github.com/evanw/esbuild/issues/3700))
36+
37+
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:
38+
39+
```js
40+
// Original code
41+
import data from 'data:application/json,{"__proto__":{"fail":true}}'
42+
if (Object.getPrototypeOf(data)?.fail) throw 'fail'
43+
44+
// Old output (with --bundle)
45+
(() => {
46+
// <data:application/json,{"__proto__":{"fail":true}}>
47+
var json_proto_fail_true_default = { __proto__: { fail: true } };
48+
49+
// entry.js
50+
if (Object.getPrototypeOf(json_proto_fail_true_default)?.fail)
51+
throw "fail";
52+
})();
53+
54+
// New output (with --bundle)
55+
(() => {
56+
// <data:application/json,{"__proto__":{"fail":true}}>
57+
var json_proto_fail_true_default = { ["__proto__"]: { fail: true } };
58+
59+
// example.mjs
60+
if (Object.getPrototypeOf(json_proto_fail_true_default)?.fail)
61+
throw "fail";
62+
})();
63+
```
64+
3565
* Improve dead code removal of `switch` statements ([#3659](https://github.com/evanw/esbuild/issues/3659))
3666

3767
With this release, esbuild will now remove `switch` statements in branches when minifying if they are known to never be evaluated:

internal/bundler/bundler.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,9 @@ func parseFile(args parseArgs) {
244244
result.ok = true
245245

246246
case config.LoaderJSON, config.LoaderWithTypeJSON:
247-
expr, ok := args.caches.JSONCache.Parse(args.log, source, js_parser.JSONOptions{})
247+
expr, ok := args.caches.JSONCache.Parse(args.log, source, js_parser.JSONOptions{
248+
UnsupportedJSFeatures: args.options.UnsupportedJSFeatures,
249+
})
248250
ast := js_parser.LazyExportAST(args.log, source, js_parser.OptionsFromConfig(&args.options), expr, "")
249251
if loader == config.LoaderWithTypeJSON {
250252
// The exports kind defaults to "none", in which case the linker picks

internal/bundler_tests/bundler_loader_test.go

+43
Original file line numberDiff line numberDiff line change
@@ -1632,3 +1632,46 @@ func TestLoaderBundleWithTypeJSONOnlyDefaultExport(t *testing.T) {
16321632
`,
16331633
})
16341634
}
1635+
1636+
func TestLoaderJSONPrototype(t *testing.T) {
1637+
loader_suite.expectBundled(t, bundled{
1638+
files: map[string]string{
1639+
"/entry.js": `
1640+
import data from "./data.json"
1641+
console.log(data)
1642+
`,
1643+
"/data.json": `{
1644+
"": "The property below should be converted to a computed property:",
1645+
"__proto__": { "foo": "bar" }
1646+
}`,
1647+
},
1648+
entryPaths: []string{"/entry.js"},
1649+
options: config.Options{
1650+
Mode: config.ModeBundle,
1651+
AbsOutputFile: "/out.js",
1652+
MinifySyntax: true,
1653+
},
1654+
})
1655+
}
1656+
1657+
func TestLoaderJSONPrototypeES5(t *testing.T) {
1658+
loader_suite.expectBundled(t, bundled{
1659+
files: map[string]string{
1660+
"/entry.js": `
1661+
import data from "./data.json"
1662+
console.log(data)
1663+
`,
1664+
"/data.json": `{
1665+
"": "The property below should NOT be converted to a computed property for ES5:",
1666+
"__proto__": { "foo": "bar" }
1667+
}`,
1668+
},
1669+
entryPaths: []string{"/entry.js"},
1670+
options: config.Options{
1671+
Mode: config.ModeBundle,
1672+
AbsOutputFile: "/out.js",
1673+
MinifySyntax: true,
1674+
UnsupportedJSFeatures: es(5),
1675+
},
1676+
})
1677+
}

internal/bundler_tests/snapshots/snapshots_loader.txt

+24
Original file line numberDiff line numberDiff line change
@@ -899,6 +899,30 @@ TestLoaderJSONNoBundleIIFE
899899
require_test();
900900
})();
901901

902+
================================================================================
903+
TestLoaderJSONPrototype
904+
---------- /out.js ----------
905+
// data.json
906+
var data_default = {
907+
"": "The property below should be converted to a computed property:",
908+
["__proto__"]: { foo: "bar" }
909+
};
910+
911+
// entry.js
912+
console.log(data_default);
913+
914+
================================================================================
915+
TestLoaderJSONPrototypeES5
916+
---------- /out.js ----------
917+
// data.json
918+
var data_default = {
919+
"": "The property below should NOT be converted to a computed property for ES5:",
920+
__proto__: { foo: "bar" }
921+
};
922+
923+
// entry.js
924+
console.log(data_default);
925+
902926
================================================================================
903927
TestLoaderJSONSharedWithMultipleEntriesIssue413
904928
---------- /out/a.js ----------

internal/js_parser/json_parser.go

+12-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package js_parser
33
import (
44
"fmt"
55

6+
"github.com/evanw/esbuild/internal/compat"
67
"github.com/evanw/esbuild/internal/helpers"
78
"github.com/evanw/esbuild/internal/js_ast"
89
"github.com/evanw/esbuild/internal/js_lexer"
@@ -142,6 +143,14 @@ func (p *jsonParser) parseExpr() js_ast.Expr {
142143
Key: key,
143144
ValueOrNil: value,
144145
}
146+
147+
// The key "__proto__" must not be a string literal in JavaScript because
148+
// that actually modifies the prototype of the object. This can be
149+
// avoided by using a computed property key instead of a string literal.
150+
if helpers.UTF16EqualsString(keyString, "__proto__") && !p.options.UnsupportedJSFeatures.Has(compat.ObjectExtensions) {
151+
property.Flags |= js_ast.PropertyIsComputed
152+
}
153+
145154
properties = append(properties, property)
146155
}
147156

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

165174
type JSONOptions struct {
166-
Flavor js_lexer.JSONFlavor
167-
ErrorSuffix string
175+
UnsupportedJSFeatures compat.JSFeature
176+
Flavor js_lexer.JSONFlavor
177+
ErrorSuffix string
168178
}
169179

170180
func ParseJSON(log logger.Log, source logger.Source, options JSONOptions) (result js_ast.Expr, ok bool) {

scripts/end-to-end-tests.js

+37
Original file line numberDiff line numberDiff line change
@@ -3528,6 +3528,43 @@ for (const minify of [[], ['--minify-syntax']]) {
35283528
`,
35293529
}),
35303530
)
3531+
3532+
// https://github.com/evanw/esbuild/issues/3700
3533+
tests.push(
3534+
test(['in.js', '--bundle', '--outfile=node.js'].concat(minify), {
3535+
'in.js': `
3536+
import imported from './data.json'
3537+
const native = JSON.parse(\`{
3538+
"hello": "world",
3539+
"__proto__": {
3540+
"sky": "universe"
3541+
}
3542+
}\`)
3543+
const literal1 = {
3544+
"hello": "world",
3545+
"__proto__": {
3546+
"sky": "universe"
3547+
}
3548+
}
3549+
const literal2 = {
3550+
"hello": "world",
3551+
["__proto__"]: {
3552+
"sky": "universe"
3553+
}
3554+
}
3555+
if (Object.getPrototypeOf(native)?.sky) throw 'fail: native'
3556+
if (!Object.getPrototypeOf(literal1)?.sky) throw 'fail: literal1'
3557+
if (Object.getPrototypeOf(literal2)?.sky) throw 'fail: literal2'
3558+
if (Object.getPrototypeOf(imported)?.sky) throw 'fail: imported'
3559+
`,
3560+
'data.json': `{
3561+
"hello": "world",
3562+
"__proto__": {
3563+
"sky": "universe"
3564+
}
3565+
}`,
3566+
}),
3567+
)
35313568
}
35323569

35333570
// Test minification of top-level symbols

0 commit comments

Comments
 (0)