eval in reverse.
- Shared and circular references
- Every built-in type, including sparse arrays,
-0, invalidDates,null-prototype objects, property descriptors,Temporal, andTypedArrays with non-canonical NaNs - Safe to run on untrusted input and to embed in a
<script>tag - Custom types via a callback
- Passes more roundtrip tests than any other package compared
See the full list of supported types.
We prioritize these metrics in the following order:
- Security (see our guarantees)
- Correctness (i.e.
(0, eval)(`(${uneval(value)})`)roundtrips) - Generated source size (human-readable output is a non-goal)
- Generated source runtime performance
unevalruntime performanceunevalbundle size
Note that we do still care about metrics lower on the list. We just care about other metrics more.
$ npm i unevalimport assert from 'node:assert'
import uneval from 'uneval'
const object = { message: `hello world` }
const source = uneval(object)
console.log(source)
//=> {message:"hello world"}
const roundtrippedObject = (0, eval)(`(${source})`)
assert.deepEqual(roundtrippedObject, object)
const circularObject = {}
circularObject.self = circularObject
const circularSource = uneval(circularObject)
console.log(circularSource)
//=> (a=>a.self=a)({})
const roundtrippedCircularObject = (0, eval)(`(${circularSource})`)
assert.deepEqual(roundtrippedCircularObject, circularObject)Warning
We cannot ensure our security guarantees when the
custom option is used.
uneval accepts a custom callback for unevaling values.
import assert from 'node:assert'
import uneval from 'uneval'
class Person {
constructor(name) {
this.name = name
}
}
const people = {
tomer: new Person(`Tomer`),
amanda: new Person(`Amanda`),
}
const source = uneval(people, {
custom: (value, uneval) => {
if (value instanceof Person) {
return `new Person(${uneval(value.name)})`
}
},
})
console.log(source)
//=> {tomer:new Person("Tomer"),amanda:new Person("Amanda")}
const roundtrippedPeople = (0, eval)(`(${source})`)
assert.deepEqual(roundtrippedPeople, people)Return the following types depending on the desired behavior:
stringto provide custom source for the input valuenullto omit the input value from the output (e.g. in arrays, objects, Sets, Maps). Omitting the root value throws anError.undefined(or don't return anything, which is equivalent) to use the default behavior for the input value
The callback can be used to uneval already supported values differently or to
uneval unsupported values such as functions. It also receives a second uneval
param, which is the same uneval function the options were passed to. You can
use it to delegate back to uneval for sub-values.
Note
custom is only called for each logical value. It is not called for
sub-values that are incidentally unevaled as part of unevaling a value.
Some examples:
customis called for each element of anArraybecause anArrayis a container and its elements are its logical sub-values.customis called for eachDatevalue, but not for aDatevalue's underlying numerical timestamp because theDateitself is the logical value whilenew Date(numericalTimestamp)is just one way aDatecould beunevaled. Other ways are possible, which means the numerical timestamp in the source is just an implementation detail, not a logical sub-value.customis called for eachArrayBuffervalue even when it's nested within aTypedArrayor NodeBufferbecause the underlyingArrayBufferis the logical sub-value backing these parent values. The fact that it can be shared betweenTypedArrayand/or NodeBufferinstances is proof of this.
This principle is a bit hand-wavy, but we use our best judgement. If you find
a scenario where custom doesn't work the way you expect, then
create an issue.
undefinedandnullbooleannumber(including-0)stringandRegExp(including unpaired surrogates,</script>escaping, etc.)- Boxed primitives
BigInt- Shared
and
well-known
Symbol Array(including sparse arrays)Object(includingnull-prototype, arbitrary descriptors, etc.)SetMapDate(including invalid ones)TemporalURLandURLSearchParamsArrayBuffer(includingresizable,detached, andmaxByteLength)- Node
Buffer TypedArray(includingBigIntarrays,Float16Array, and float arrays with non-canonical NaNs)DataViewargumentsobject (both sloppy and strict mode)Error(including subclasses)- Shared/circular reference (for all of the above types)
- Custom type
These guarantees assume the attacker controls the input value, but not the
environment. uneval inspects values through the standard globals and prototype
methods, so replaced globals and patched prototypes are out of scope. An
attacker who can tamper with the environment already runs code in the process.
The following are safe UNLESS custom is used:
-
Running
unevalon untrusted input.unevalreturns a string and never evaluates the input, so converting an untrusted value runs no attacker code throughunevalitself.Converting a value reads it, though, so a coercion method the input defines on itself runs while
unevalinspects it (e.g. its ownvalueOf). That runs the input's own code, but the returned string stays safe. Treat converting untrusted input the way you would treatJSON.stringifyof the same input, which runs the input's owntoJSON. -
Running
(0, eval)(`(${uneval(value)})`).Call
evalindirectly, as(0, eval)(...), so the evaluated source runs in the global scope instead of the surrounding one and cannot read local variables.new Function(...)runs in the global scope for the same reason. -
Embedding
uneval(value)in JS source code, including inside an HTML<script>tag.We always escape
</script>to avoid the following XSS attack:const value = { untrustedInput: `</script><script src='https://evil.com/hacked.js'>`, } const html = ` <script> var preloaded = ${uneval(value)}; </script> `
Without escaping, we'd end up with this (after formatting):
<script> var preloaded = {untrustedInput:" </script> <!-- Oh no! We've loaded an evil script :( --> <script src="https://evil.com/hacked.js"> "} </script>
But with escaping we get:
<script> var preloaded = { untrustedInput: "\u003c/script>\u003cscript src='https://evil.com/hacked.js'>", } </script>
-
Running
unevalon a pooledBuffer.unevaling the underlyingArrayBufferof a pooledBufferand using it in the output source would be more correct from a roundtripping perspective, but wouldn't be safe because the underlyingArrayBuffermay contain sensitive data from otherBuffers.Instead, a
Bufferisunevaled with a slicedArrayBuffercontaining only the data in its view if theArrayBuffer's size matchesBuffer.poolSize. This results in false-positives forBuffers containingArrayBuffers that coincidentally have a size ofBuffer.poolSize.To force an
ArrayBufferto beunevaled as-is, include it elsewhere in the input to signal that it's expected to be in the output. For example:const poolSizeArrayBuffer = new ArrayBuffer(Buffer.poolSize) const source = uneval([ poolSizeArrayBuffer, Buffer.from(poolSizeArrayBuffer, 5, 3), ]) console.log(source) //=> (a=>[a,Buffer.from(a,5,3)])(new ArrayBuffer(8192))
The tables are auto-generated.
docs/comparison.md shows the result of each
roundtrip test for each package.
The table below shows the results of unevaling the
same 5,000 randomly generated values with each package. The numbers are from one
machine and vary between runs. Each row also shows how many of the roundtrip
tests the package passes, because a faster package may pass fewer of them.
| Package | Tests passing | Ops/sec | Mean | Relative |
|---|---|---|---|---|
uneval |
đąÂ 505/515 | 134 | 7.51ms ±1.98% | 2.07Ă slower |
seroval@1â .â 6â .â 4Â (sync) |
đĄÂ 365/515 | 124 | 8.15ms ±2.48% | 2.24Ă slower |
devalue@5â .â 9â .â 2 |
đĄÂ 363/515 | 82.7 | 12.11ms ±1.08% | 3.36Ă slower |
javascriptâ -â stringify@2â .â 1â .â 0 |
đ  213/515 | 85.0 | 11.85ms ±2.22% | 3.27Ă slower |
serializeâ -â javascript@7â .â 1â .â 1 |
đ  156/515 | 111 | 9.06ms ±1.08% | 2.51Ă slower |
jsesc@3â .â 1â .â 0 |
đ  141/515 | 36.0 | 27.77ms ±0.88% | 7.71Ă slower |
tosource@2â .â 0â .â 0â -â alphaâ .â 3 |
đ  141/515 | 134 | 7.50ms ±1.22% | 2.08Ă slower |
jsâ -â stringify@1â .â 0â .â 2 |
đŽÂ 71/515 | 278 | 3.61ms ±0.75% | fastest |
Found an inaccuracy? Please create an issue or a pull request!
Stars are always welcome!
For bugs and feature requests, please create an issue.
Special thanks to Chakrit Wichian for donating the package name!
MIT ©