Security News
Research
Data Theft Repackaged: A Case Study in Malicious Wrapper Packages on npm
The Socket Research Team breaks down a malicious wrapper package that uses obfuscation to harvest credentials and exfiltrate sensitive data.
Serialization library that allows arbitrary objects and classes to be serialized or deserialized in a safe way.
kaiser can serialize standard JavaScript data, objects, arrays and dates out of the box.
However, kaiser is not magical. Any non-native object type has to be registered with a type id (ideally a uuid), a serializer and a deserializer.
General: kaiser can be configured to serialize and deserialize any data.
Powerful: object identity is preserved by deserialization. Circular references can be handled.
Safe(ish): the deserializer can be configured to only deserialize objects from a whitelist. This is safe as long as the serializers in the whitelist are safe.
Low registration overhead: packages can register custom
serializers for their classes and functions using kaiser/reg
,
which is about 300 bytes minified.
Other uses: kaiser can be used to make shallow or deep copies of objects.
The kaiser
package in and of itself is fairly large for what it
does, mostly because it is written in Earl Grey, which comes with some
overhead. I plan to address that eventually.
Simple serialization/deserialization
var kaiser = require("kaiser");
var str = kaiser.serialize({a: 1, b: new Date()});
var obj = kaiser.deserialize(str);
You can also instantiate a Serializer
object with a whitelist, which
means only a limited list of approved objects and classes can be
serialized or deserialized. The resulting serializer, when used on
unsanitized data, will be as safe as the most unsafe object in the
whitelist, so be careful:
var kaiser = require("kaiser");
var s = kaiser.Serializer([Date, Person]);
var str1 = ser.serialize({a: new Date(), b: new Person()}); // OK!
var obj2 = ser.deserialize(str1); // OK!
var str2 = ser.serialize({a: 1, b: new Animal()}); // ERROR!
var str3 = kaiser.serialize({a: 1, b: new Animal()}); // OK (generic serializer)
var obj3 = ser.deserialize(str3); // ERROR!
To register with kaiser
you need to assign a uuid to your class or
function. A uuid is a unique symbol which can identify your
functionality in any application that imports it and will not clash
with any other package. There are a few ways to do it, and kaiser
can help you.
First you must import kaiser
, for that you have two options:
// Option A: Import the full package
kaiser = require("kaiser");
// Option B: Import only the registering functions
kaiser = require("kaiser/reg");
The difference between the two is that option B only defines a few
stubs to let you register your classes for serialization and is about
300 bytes minified, so it is super cheap if you only want to provide
the functionality to people who need it. Then when they import the
full kaiser
package, the serializers will get registered for real.
Now, suppose you have the following definition:
function Vehicle(brand) {
this.brand = brand;
}
Vehicle.prototype.start = function () {
console.log("vroom vroom!");
}
This will assign Vehicle
the uuid npm:my-package/Vehicle
:
kaiser.register(Vehicle.prototype, {
package: {name: "my-package", "version": "1.2.3"}
});
This will assign Vehicle
the uuid npm:my-package@1/Vehicle
:
kaiser.register(Vehicle.prototype, {
package: {name: "my-package", "version": "1.2.3"},
useVersion: "major"
});
Of course, if you have a package.json
file in the same directory,
you should simply do this:
kaiser.register(Vehicle.prototype, {
package: require("./package.json"),
useVersion: "minor"
});
To register more than one class, use kaiser.registerAll
:
kaiser.registerAll([Vehicle.prototype, Animal.prototype], {
package: require("./package.json"),
useVersion: "minor"
});
Install the uuid
command on your system and run it. It will give you
a (presumably) unique hexadecimal identifier that you can paste in
your code:
$ uuid
bfd249d8-302a-11e5-8044-278351ad39e9
Then you must set the typeId
field:
kaiser.register(Vehicle.prototype, {
typeId: "bfd249d8-302a-11e5-8044-278351ad39e9"
});
kaiser.registerAll
can be used with just one typeId
. What it will
do is that it will generate ids like
bfd249d8-302a-11e5-8044-278351ad39e9/Vehicle
and so on.
In order to generate a uuid, kaiser
follows these steps:
typeId
and variant
: typeId + JSON.stringify(variant)
typeId
and nameVariant === true
: typeId + object.name
typeId
and nameVariant
: typeId + nameVariant
typeId
: typeId
package
and useVersion
: package@version/object.name
Note that registerAll
and registerSingletons
automatically set
nameVariant
in order to differentiate the entries. It is thus
important that they all have names.
By default, kaiser
understands that what is being registered is a
prototype, and that objects that directly inherit from the prototype
are those that we wish to serialize.
On the other hand, you may want to serialize functions, or objects
as-is. To that purpose you may use kaiser.registerFunction
or
kaiser.registerSingleton
(they are the same thing). Both functions
have a plural equivalent that lets you register more than one thing at
once.
function hello(name) {
return "hello, " + name
}
function bye(name) {
return "bye, " + name
}
kaiser.registerSingletons([hello, bye], {
package: require("./package.json"),
useVersion: "patch"
});
The above will registers ids "npm:my-package@1.2.3/hello"
and
"npm:my-package@1.2.3/bye"
.
By default kaiser.serialize
will pack all the fields in an object
and kaiser.deserialize
will instantiate an object with the right
prototype and write the fields back.
But you can change this. Here is a simple example (of a flawed serialization mechanism):
kaiser.register(Person.prototype, {
typeId: "bfd249d8-302a-11e5-8044-278351ad39e9",
serialize: function (person) {
return person.name + "/" + person.age;
},
deserialize: function (person) {
fields = person.split("/");
return new Person(fields[0], parseInt(fields[1]));
}
});
The serialization and deserialization interface is as follows:
serialize(object)
must return either some primitive type like a
String or Number, or a plain object or array in which the fields are
not serialized (in other words, do not call kaiser.serialize
in
that function). kaiser
will serialize these fields for you so that
you can focus on the logic.
deserialize(form)
must rebuild the object previously serialized
exactly. It will receive the same output serialize produced, with
already deserialized fields (do not call kaiser.deserialize
in that
function).
In addition, the following two methods must be implemented to support circular references for your type:
create()
must return an instance of the object.
fill(target, form)
must fill the return value of create
so
that target
becomes the deserialized form
.
To put it simply, var r = create(); fill(r, x)
must be equivalent to
var r = deserialize(x)
.
FAQs
Serialization of arbitrary objects
The npm package kaiser receives a total of 11,256 weekly downloads. As such, kaiser popularity was classified as popular.
We found that kaiser demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Research
The Socket Research Team breaks down a malicious wrapper package that uses obfuscation to harvest credentials and exfiltrate sensitive data.
Research
Security News
Attackers used a malicious npm package typosquatting a popular ESLint plugin to steal sensitive data, execute commands, and exploit developer systems.
Security News
The Ultralytics' PyPI Package was compromised four times in one weekend through GitHub Actions cache poisoning and failure to rotate previously compromised API tokens.