-
Notifications
You must be signed in to change notification settings - Fork 29.6k
Commit
This enables code loaded via the module system to be checked for integrity to ensure the code loaded matches expectations. PR-URL: #23834 Reviewed-By: Guy Bedford <[email protected]> Reviewed-By: Vladimir de Turckheim <[email protected]> Reviewed-By: Matteo Collina <[email protected]>
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
# Policies | ||
|
||
<!--introduced_in=TODO--> | ||
<!-- type=misc --> | ||
|
||
> Stability: 1 - Experimental | ||
<!-- name=policy --> | ||
|
||
Node.js contains experimental support for creating policies on loading code. | ||
|
||
Policies are a security feature intended to allow guarantees | ||
about what code Node.js is able to load. The use of policies assumes | ||
safe practices for the policy files such as ensuring that policy | ||
files cannot be overwritten by the Node.js application by using | ||
file permissions. | ||
|
||
A best practice would be to ensure that the policy manifest is read only for | ||
the running Node.js application, and that the file cannot be changed | ||
by the running Node.js application in any way. A typical setup would be to | ||
create the policy file as a different user id than the one running Node.js | ||
and granting read permissions to the user id running Node.js. | ||
|
||
## Enabling | ||
|
||
<!-- type=misc --> | ||
|
||
The `--experimental-policy` flag can be used to enable features for policies | ||
when loading modules. | ||
|
||
Once this has been set, all modules must conform to a policy manifest file | ||
passed to the flag: | ||
|
||
```sh | ||
node --experimental-policy=policy.json app.js | ||
``` | ||
|
||
The policy manifest will be used to enforce constraints on code loaded by | ||
Node.js. | ||
|
||
## Features | ||
|
||
### Error Behavior | ||
|
||
When a policy check fails, Node.js by default will throw an error. | ||
It is possible to change the error behavior to one of a few possibilities | ||
by defining an "onerror" field in a policy manifest. The following values are | ||
available to change the behavior: | ||
|
||
* `"exit"` - will exit the process immediately. | ||
No cleanup code will be allowed to run. | ||
* `"log"` - will log the error at the site of the failure. | ||
* `"throw"` (default) - will throw a JS error at the site of the failure. | ||
|
||
```json | ||
{ | ||
"onerror": "log", | ||
"resources": { | ||
"./app/checked.js": { | ||
"integrity": "sha384-SggXRQHwCG8g+DktYYzxkXRIkTiEYWBHqev0xnpCxYlqMBufKZHAHQM3/boDaI/0" | ||
} | ||
} | ||
} | ||
``` | ||
|
||
### Integrity Checks | ||
|
||
Policy files must use integrity checks with Subresource Integrity strings | ||
compatible with the browser | ||
[integrity attribute](https://www.w3.org/TR/SRI/#the-integrity-attribute) | ||
associated with absolute URLs. | ||
|
||
When using `require()` all resources involved in loading are checked for | ||
integrity if a policy manifest has been specified. If a resource does not match | ||
the integrity listed in the manifest, an error will be thrown. | ||
|
||
An example policy file that would allow loading a file `checked.js`: | ||
|
||
```json | ||
{ | ||
"resources": { | ||
"./app/checked.js": { | ||
"integrity": "sha384-SggXRQHwCG8g+DktYYzxkXRIkTiEYWBHqev0xnpCxYlqMBufKZHAHQM3/boDaI/0" | ||
} | ||
} | ||
} | ||
``` | ||
|
||
Each resource listed in the policy manifest can be of one the following | ||
formats to determine its location: | ||
|
||
1. A [relative url string][] to a resource from the manifest such as `./resource.js`, `../resource.js`, or `/resource.js`. | ||
2. A complete url string to a resource such as `file:///resource.js`. | ||
|
||
When loading resources the entire URL must match including search parameters | ||
and hash fragment. `./a.js?b` will not be used when attempting to load | ||
`./a.js` and vice versa. | ||
|
||
In order to generate integrity strings, a script such as | ||
`printf "sha384-$(cat checked.js | openssl dgst -sha384 -binary | base64)"` | ||
can be used. | ||
|
||
|
||
[relative url string]: https://url.spec.whatwg.org/#relative-url-with-fragment-string |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,8 +22,8 @@ | |
'use strict'; | ||
|
||
const { NativeModule } = require('internal/bootstrap/loaders'); | ||
const util = require('util'); | ||
const { pathToFileURL } = require('internal/url'); | ||
const util = require('util'); | ||
const vm = require('vm'); | ||
const assert = require('assert').ok; | ||
const fs = require('fs'); | ||
|
@@ -45,6 +45,9 @@ const { getOptionValue } = require('internal/options'); | |
const preserveSymlinks = getOptionValue('--preserve-symlinks'); | ||
const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main'); | ||
const experimentalModules = getOptionValue('--experimental-modules'); | ||
const manifest = getOptionValue('[has_experimental_policy]') ? | ||
require('internal/process/policy').manifest : | ||
null; | ||
|
||
const { | ||
ERR_INVALID_ARG_VALUE, | ||
|
@@ -168,6 +171,11 @@ function readPackage(requestPath) { | |
return false; | ||
} | ||
|
||
if (manifest) { | ||
const jsonURL = pathToFileURL(jsonPath); | ||
manifest.assertIntegrity(jsonURL, json); | ||
} | ||
|
||
try { | ||
return packageMainCache[requestPath] = JSON.parse(json).main; | ||
} catch (e) { | ||
|
@@ -676,6 +684,10 @@ function normalizeReferrerURL(referrer) { | |
// the file. | ||
// Returns exception, if any. | ||
Module.prototype._compile = function(content, filename) { | ||
if (manifest) { | ||
const moduleURL = pathToFileURL(filename); | ||
manifest.assertIntegrity(moduleURL, content); | ||
} | ||
|
||
content = stripShebang(content); | ||
|
||
|
@@ -715,11 +727,14 @@ Module.prototype._compile = function(content, filename) { | |
var depth = requireDepth; | ||
if (depth === 0) stat.cache = new Map(); | ||
var result; | ||
var exports = this.exports; | ||
var thisValue = exports; | ||
var module = this; | ||
if (inspectorWrapper) { | ||
result = inspectorWrapper(compiledWrapper, this.exports, this.exports, | ||
require, this, filename, dirname); | ||
result = inspectorWrapper(compiledWrapper, thisValue, exports, | ||
require, module, filename, dirname); | ||
} else { | ||
result = compiledWrapper.call(this.exports, this.exports, require, this, | ||
result = compiledWrapper.call(thisValue, exports, require, module, | ||
filename, dirname); | ||
} | ||
if (depth === 0) stat.cache = null; | ||
|
@@ -736,7 +751,13 @@ Module._extensions['.js'] = function(module, filename) { | |
|
||
// Native extension for .json | ||
Module._extensions['.json'] = function(module, filename) { | ||
var content = fs.readFileSync(filename, 'utf8'); | ||
const content = fs.readFileSync(filename, 'utf8'); | ||
|
||
if (manifest) { | ||
const moduleURL = pathToFileURL(filename); | ||
manifest.assertIntegrity(moduleURL, content); | ||
} | ||
|
||
try { | ||
module.exports = JSON.parse(stripBOM(content)); | ||
} catch (err) { | ||
|
@@ -748,6 +769,12 @@ Module._extensions['.json'] = function(module, filename) { | |
|
||
// Native extension for .node | ||
Module._extensions['.node'] = function(module, filename) { | ||
if (manifest) { | ||
const content = fs.readFileSync(filename); | ||
const moduleURL = pathToFileURL(filename); | ||
manifest.assertIntegrity(moduleURL, content); | ||
} | ||
// be aware this doesn't use `content` | ||
return process.dlopen(module, path.toNamespacedPath(filename)); | ||
This comment has been minimized.
Sorry, something went wrong.
This comment has been minimized.
Sorry, something went wrong.
bmeck
Author
Member
|
||
}; | ||
|
||
|
This looks like a TOCTOU race condition which we should be very careful with security-relevant code. Are we really okay with this?