policy: add policy-integrity to mitigate policy tampering · nodejs/node@2eeb44f · GitHub
Skip to content

Commit 2eeb44f

Browse files
bmecktargos
authored andcommitted
policy: add policy-integrity to mitigate policy tampering
PR-URL: #28734 Reviewed-By: Gus Caplan <me@gus.host> Reviewed-By: Richard Lau <riclau@uk.ibm.com> Reviewed-By: Guy Bedford <guybedford@gmail.com> Reviewed-By: Colin Ihrig <cjihrig@gmail.com> Reviewed-By: Rich Trott <rtrott@gmail.com>
1 parent cf811ec commit 2eeb44f

9 files changed

Lines changed: 148 additions & 0 deletions

File tree

doc/api/cli.md

Lines changed: 13 additions & 0 deletions

doc/api/policy.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,15 @@ node --experimental-policy=policy.json app.js
3838
The policy manifest will be used to enforce constraints on code loaded by
3939
Node.js.
4040

41+
In order to mitigate tampering with policy files on disk, an integrity for
42+
the policy file itself may be provided via `--policy-integrity`.
43+
This allows running `node` and asserting the policy file contents
44+
even if the file is changed on disk.
45+
46+
```sh
47+
node --experimental-policy=policy.json --policy-integrity="sha384-SggXRQHwCG8g+DktYYzxkXRIkTiEYWBHqev0xnpCxYlqMBufKZHAHQM3/boDaI/0" app.js
48+
```
49+
4150
## Features
4251

4352
### Error Behavior

doc/node.1

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,9 @@ Among other uses, this can be used to enable FIPS-compliant crypto if Node.js is
231231
.It Fl -pending-deprecation
232232
Emit pending deprecation warnings.
233233
.
234+
.It Fl -policy-integrity Ns = Ns Ar sri
235+
Instructs Node.js to error prior to running any code if the policy does not have the specified integrity. It expects a Subresource Integrity string as a parameter.
236+
.
234237
.It Fl -preserve-symlinks
235238
Instructs the module loader to preserve symbolic links when resolving and caching modules other than the main module.
236239
.

lib/internal/bootstrap/pre_execution.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const { Object, SafeWeakMap } = primordials;
44

55
const { getOptionValue } = require('internal/options');
66
const { Buffer } = require('buffer');
7+
const { ERR_MANIFEST_ASSERT_INTEGRITY } = require('internal/errors').codes;
78

89
function prepareMainThreadExecution(expandArgv1 = false) {
910
// Patch the process object with legacy properties and normalizations
@@ -332,6 +333,32 @@ function initializePolicy() {
332333
}
333334
const fs = require('fs');
334335
const src = fs.readFileSync(manifestURL, 'utf8');
336+
const experimentalPolicyIntegrity = getOptionValue('--policy-integrity');
337+
if (experimentalPolicyIntegrity) {
338+
const SRI = require('internal/policy/sri');
339+
const { createHash, timingSafeEqual } = require('crypto');
340+
const realIntegrities = new Map();
341+
const integrityEntries = SRI.parse(experimentalPolicyIntegrity);
342+
let foundMatch = false;
343+
for (var i = 0; i < integrityEntries.length; i++) {
344+
const {
345+
algorithm,
346+
value: expected
347+
} = integrityEntries[i];
348+
const hash = createHash(algorithm);
349+
hash.update(src);
350+
const digest = hash.digest();
351+
if (digest.length === expected.length &&
352+
timingSafeEqual(digest, expected)) {
353+
foundMatch = true;
354+
break;
355+
}
356+
realIntegrities.set(algorithm, digest.toString('base64'));
357+
}
358+
if (!foundMatch) {
359+
throw new ERR_MANIFEST_ASSERT_INTEGRITY(manifestURL, realIntegrities);
360+
}
361+
}
335362
require('internal/process/policy')
336363
.setup(src, manifestURL.href);
337364
}

src/node_options.cc

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,13 @@ void EnvironmentOptions::CheckOptions(std::vector<std::string>* errors) {
116116
if (!userland_loader.empty() && !experimental_modules) {
117117
errors->push_back("--loader requires --experimental-modules be enabled");
118118
}
119+
if (has_policy_integrity_string && experimental_policy.empty()) {
120+
errors->push_back("--policy-integrity requires "
121+
"--experimental-policy be enabled");
122+
}
123+
if (has_policy_integrity_string && experimental_policy_integrity.empty()) {
124+
errors->push_back("--policy-integrity cannot be empty");
125+
}
119126

120127
if (!module_type.empty()) {
121128
if (!experimental_modules) {
@@ -321,6 +328,15 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
321328
"security policy",
322329
&EnvironmentOptions::experimental_policy,
323330
kAllowedInEnvironment);
331+
AddOption("[has_policy_integrity_string]",
332+
"",
333+
&EnvironmentOptions::has_policy_integrity_string);
334+
AddOption("--policy-integrity",
335+
"ensure the security policy contents match "
336+
"the specified integrity",
337+
&EnvironmentOptions::experimental_policy_integrity,
338+
kAllowedInEnvironment);
339+
Implies("--policy-integrity", "[has_policy_integrity_string]");
324340
AddOption("--experimental-repl-await",
325341
"experimental await keyword support in REPL",
326342
&EnvironmentOptions::experimental_repl_await,

src/node_options.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ class EnvironmentOptions : public Options {
106106
bool experimental_wasm_modules = false;
107107
std::string module_type;
108108
std::string experimental_policy;
109+
std::string experimental_policy_integrity;
110+
bool has_policy_integrity_string;
109111
bool experimental_repl_await = false;
110112
bool experimental_vm_modules = false;
111113
bool expose_internals = false;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"resources": {
3+
"./dep.js": {
4+
"integrity": "sha512-7CMcc2oytFfMnGQaXbJk84gYWF2J7p/fmWPW7dsnJyniD+vgxtK9VAZ/22UxFOA4q5d27RoGLxSqNZ/nGCJkMw== sha512-scgN9Td0bGMlGH2lUHvEeHtz92Hx6AO+sYhU3WRI6bn3jEUCXbXJs68nOOsGzRWR7a2tbqGoETnOCpHHf1Njhw=="
5+
}
6+
}
7+
}

test/fixtures/policy/dep.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
'use strict';
2+
module.exports = 'The Secret Ingredient';
Lines changed: 69 additions & 0 deletions

0 commit comments

Comments
 (0)