-
Notifications
You must be signed in to change notification settings - Fork 3
/
resource-integrity-hook.js
144 lines (128 loc) · 4.39 KB
/
resource-integrity-hook.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
/**
* @license
* Copyright 2018 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview
* A factory for hooks that prevent require of files not on a production whitelist
* such as that generated by scripts/generate-production-source-list.js
*/
'use strict';
// SENSITIVE - Trusted to preserve guarantee that the only code that
// loads in production was written or `npm install`ed by a trusted
// developer for use in production.
// GUARANTEE - the output of makeHook only returns a module M if that
// the hash of the content at require.resolve(M) appears on the production
// whitelist.
// This guarantee should survive in the face of an attacker who can
// create files including symlinks and hardlinks but makes no
// guarantee in the face of race conditions related to renaming
// ancestor directories, replacing file content, or unmounting file
// systems.
// TODO: do we need to check the hash of ./innocuous.js's before
// returning it? That seems a vector that doesn't require winning
// a race condition.
//
// TODO: document caveats related to overwrites.
//
// If we cache hashes, we are assuming no overwrites and cannot trust
// mtime more than we trust content.
//
// But commonly used modules assume the path to the require cache is fast,
// so we might have to be optimistic and just tell people to run node as
// a uid that cannot write source files if they want resource integrity
// to be strong.
//
// TODO: Figure out how to phrase this guarantee optimistically assuming
// some care taken with file permissions.
//
// TODO: Maybe gather stats on how many modules are required more than
// once and lazily: lib/handlers?
//
// TODO: Do we reduce the attack surface by having startup scripts
// `chmod -R ugo-w lib node_modules` even if the server process owns
// its source files?
// eslint-disable-next-line no-use-before-define
exports.makeHook = makeHook;
const {
createHash,
Hash: {
prototype: {
digest: digestHash,
update: updateHash,
},
},
} = require('crypto');
const { readFileSync } = require('fs');
const { join, relative } = require('path');
const { isBuiltinModuleId } = require('../builtin-module-ids.js');
const { create, hasOwnProperty } = Object;
const { apply } = Reflect;
// eslint-disable-next-line id-blacklist
const { error: consoleerror, warn: consolewarn } = console;
function makeHook(hashesToSourceLists, basedir, reportOnly) {
const moduleid = relative(basedir, module.filename);
if (!(hashesToSourceLists && typeof hashesToSourceLists === 'object')) {
hashesToSourceLists = create(null);
}
// Don't hash modules more than once.
// Assumes that modules don't change on disk.
const hashCache = new Map();
function hashFor(file) {
if (hashCache.has(file)) {
return hashCache.get(file);
}
let key = null;
let content = null;
try {
content = readFileSync(file);
} catch (exc) {
consoleerror(`${ moduleid }: ${ exc.message }`);
}
if (content !== null) {
const hash = createHash('sha256');
key = apply(
digestHash,
apply(updateHash, hash, [ content ]),
[ 'hex' ]);
}
hashCache.set(file, key);
return key;
}
return function resourceIntegrityHook(
importingFile, importingId, requiredId, resolveFilename) {
if (isBuiltinModuleId(requiredId)) {
return requiredId;
}
let target = null;
try {
target = resolveFilename(requiredId);
} catch (exc) {
// We fall-through to hash mismatch below.
}
if (target !== null) {
const key = hashFor(target);
if (key && apply(hasOwnProperty, hashesToSourceLists, [ key ])) {
return requiredId;
}
}
consolewarn(
`${ moduleid }: Blocking require(${ JSON.stringify(requiredId) }) by ${ relative(basedir, importingFile) }`);
if (reportOnly) {
return requiredId;
}
return join(__dirname, 'innocuous.js');
};
}