├── static ├── hello-world.html ├── favicon.ico ├── smiley.png ├── .well-known │ └── security.txt ├── common.js └── styles.css ├── .coveralls.yml ├── doc └── images │ └── arrow-to-fork-button.png ├── scripts ├── validate.sh ├── license-check.sh ├── gen-vulnerable-patch.sh ├── postinstall.sh ├── test.sh ├── preinstall.sh ├── build-vulnerable.sh └── markdown-table-of-contents.js ├── lib ├── handlers │ ├── error.pug │ ├── four-oh-four.pug │ ├── echo.pug │ ├── post.pug │ ├── index.pug │ ├── logout.pug │ ├── includes │ │ ├── envelope.pug │ │ └── post-common.pug │ ├── login.pug │ ├── four-oh-four.js │ ├── client-error.js │ ├── error.js │ ├── account.pug │ ├── index.js │ ├── echo.js │ ├── logout.js │ ├── account.js │ └── login.js ├── framework │ ├── is-prod.js │ ├── unprivileged-require.js │ ├── builtin-module-ids.js │ ├── well-formedness-check.js │ ├── module-hooks │ │ ├── innocuous.js │ │ └── resource-integrity-hook.js │ ├── code-loading-function-proxy.js │ ├── bootstrap-secure.js │ ├── init-hooks.js │ ├── delicate-globals-rewrite.js │ ├── module-stubs.js │ └── lockdown.js ├── safe │ ├── pg.js │ ├── child_process.js │ └── html.js ├── poorly-written-linkifier.js └── db-tables.js ├── .travis.yml ├── patches ├── node_modules-fsext-fsext.patch └── node_modules-depd-index.patch ├── test ├── ok.js ├── may-use-unsafe.js ├── may-use-childprocess.js ├── file-with-known-hash.js ├── file-with-wrong-hash.js ├── may-not-use.js ├── unsafe.js ├── main-test.js ├── cases │ └── end-to-end │ │ ├── static-case.js │ │ ├── client-error-case.js │ │ ├── no-such-file-case.js │ │ ├── echo-case.js │ │ ├── index-case.js │ │ ├── login-case.js │ │ ├── login-logout-case.js │ │ └── drive-by-post-case.js ├── run-hook.js ├── end-to-end-vulnerable-test.js ├── end-to-end-lockeddown-test.js ├── init-hooks-test.js ├── code-loading-function-proxy-test.js ├── lockdown-test.js ├── poorly-written-linkifier-test.js ├── collected-db-test.js ├── resource-integrity-hook-test.js ├── db-tables-test.js ├── doc-test.js ├── end-to-end-test.js ├── external-process-test-server.js └── sensitive-module-hook-test.js ├── .gitignore ├── .github └── ISSUE_TEMPLATE │ └── breach.md ├── main.js └── package.json /static/hello-world.html: -------------------------------------------------------------------------------- 1 | 2 | Hello, World! 3 | -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-pro 2 | repo_token: vm2zPzSv4RxndXeTGVOaj1RD3wD9NxW9s 3 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanbuck/attack-review-testbed/master/static/favicon.ico -------------------------------------------------------------------------------- /static/smiley.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanbuck/attack-review-testbed/master/static/smiley.png -------------------------------------------------------------------------------- /doc/images/arrow-to-fork-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanbuck/attack-review-testbed/master/doc/images/arrow-to-fork-button.png -------------------------------------------------------------------------------- /scripts/validate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Collects sanity checks for travis-ci. 4 | 5 | set -e 6 | 7 | npm run prepack 8 | npm run license 9 | -------------------------------------------------------------------------------- /lib/handlers/error.pug: -------------------------------------------------------------------------------- 1 | mixin title() 2 | | Error 3 | 4 | mixin body() 5 | if isProduction 6 | | Something went wrong 7 | else 8 | = String(exc) 9 | 10 | include includes/envelope.pug 11 | -------------------------------------------------------------------------------- /lib/handlers/four-oh-four.pug: -------------------------------------------------------------------------------- 1 | mixin title() 2 | | File Not Found: 3 | | 4 | = reqUrl.pathname 5 | 6 | mixin body() 7 | h1 404 8 | p 9 | | Oops! Nothing at 10 | | 11 | = reqUrl 12 | 13 | include includes/envelope.pug 14 | -------------------------------------------------------------------------------- /lib/handlers/echo.pug: -------------------------------------------------------------------------------- 1 | mixin title() 2 | | Database Echo 3 | 4 | mixin body() 5 | h1 6 | | Echo 7 | table.echo 8 | tr 9 | each col in result 10 | th= col[0] 11 | tr 12 | each col in result 13 | td= col[1] 14 | 15 | include includes/envelope.pug 16 | -------------------------------------------------------------------------------- /scripts/license-check.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | MISSING="$(find lib scripts test main.js -name \*.js | xargs grep -c 'Apache' | perl -ne 'print if s/:0$//')" 6 | 7 | if [ -n "$MISSING" ]; then 8 | echo Need license headers in 9 | echo "$MISSING" 10 | exit 1 11 | fi 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | - "9" 5 | - "10" 6 | - "11" 7 | 8 | # Use faster Docker architecture on Travis. 9 | sudo: false 10 | 11 | addons: 12 | postgresql: "9.6" 13 | 14 | script: travis_retry ./scripts/validate.sh 15 | after_success: travis_retry npm run coveralls 16 | -------------------------------------------------------------------------------- /static/.well-known/security.txt: -------------------------------------------------------------------------------- 1 | Contact: https://github.com/mikesamuel/attack-review-testbed#hdr-reporting-and-verifying-a-breach 2 | Acknowledgements: https://github.com/mikesamuel/attack-review-testbed/issues?q=is%3Aissue+tag%3Afull-breach 3 | Permission: https://github.com/mikesamuel/attack-review-testbed#do-not------------------------------ 4 | -------------------------------------------------------------------------------- /lib/handlers/post.pug: -------------------------------------------------------------------------------- 1 | include includes/post-common.pug 2 | 3 | mixin title() 4 | | New Post 5 | 6 | mixin body() 7 | if preview 8 | h1 9 | | Preview 10 | ol.posts.preview 11 | li 12 | +post(preview) 13 | else 14 | h1 15 | | Post 16 | +post_form(preview) 17 | br 18 | 19 | include includes/envelope.pug 20 | -------------------------------------------------------------------------------- /lib/handlers/index.pug: -------------------------------------------------------------------------------- 1 | include includes/post-common.pug 2 | 3 | mixin title() 4 | | Attack Review Testbed 5 | 6 | mixin body() 7 | if viewAsPublic 8 | .banner.view-as-public 9 | h1 10 | | Recent Posts 11 | ol.posts 12 | for post in posts 13 | li 14 | +post(post) 15 | div 16 | a(href='/post') 17 | button(type='button') + New Post 18 | 19 | include includes/envelope.pug 20 | -------------------------------------------------------------------------------- /lib/handlers/logout.pug: -------------------------------------------------------------------------------- 1 | mixin title() 2 | | Logout 3 | 4 | mixin body() 5 | h1 6 | | Logout 7 | 8 | center 9 | form(method="POST" action="/logout" id="logout") 10 | input(type="hidden" name="cont" value=cont) 11 | button(type="submit" form="logout") Logout 12 | | 13 | | 14 | a(href=(reqUrl.searchParams.get('cont') || '/')) 15 | button(type="button") Cancel 16 | 17 | include includes/envelope.pug 18 | -------------------------------------------------------------------------------- /patches/node_modules-fsext-fsext.patch: -------------------------------------------------------------------------------- 1 | --- a/node_modules/fs-ext/fs-ext.js 2018-06-11 10:41:18.000000000 -0400 2 | +++ b/node_modules/fs-ext/fs-ext.js 2018-11-05 15:24:02.000000000 -0500 3 | @@ -137,9 +137,11 @@ 4 | } 5 | 6 | // put constants into constants module (don't like doing this but...) 7 | -for (key in binding) { 8 | - if (/^[A-Z_]+$/.test(key) && !constants.hasOwnProperty(key)) { 9 | - constants[key] = binding[key]; 10 | +if (!Object.isExtensible || Object.isExtensible(constants)) { 11 | + for (key in binding) { 12 | + if (/^[A-Z_]+$/.test(key) && !constants.hasOwnProperty(key)) { 13 | + constants[key] = binding[key]; 14 | + } 15 | } 16 | } 17 | 18 | -------------------------------------------------------------------------------- /test/ok.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | 'use strict'; 19 | 20 | module.exports = 'ok'; 21 | -------------------------------------------------------------------------------- /test/may-use-unsafe.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | 'use strict'; 19 | 20 | require('./unsafe.js'); 21 | -------------------------------------------------------------------------------- /test/may-use-childprocess.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | 'use strict'; 19 | 20 | require('child_process'); 21 | -------------------------------------------------------------------------------- /test/file-with-known-hash.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | 'use strict'; 19 | 20 | module.exports = 'Hello, World!'; 21 | -------------------------------------------------------------------------------- /test/file-with-wrong-hash.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | 'use strict'; 19 | 20 | module.exports = 'Hello, you!'; 21 | -------------------------------------------------------------------------------- /test/may-not-use.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | 'use strict'; 19 | 20 | // eslint-disable-next-line global-require 21 | module.exports = (x) => require(x); 22 | -------------------------------------------------------------------------------- /test/unsafe.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | 'use strict'; 19 | 20 | module.exports.explodesIfTouched = { 21 | touch() { 22 | throw 'explode'; // eslint-disable-line 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /lib/handlers/includes/envelope.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | title 5 | +title() 6 | script(src='/common.js') 7 | link(rel='stylesheet' href='/styles.css') 8 | body 9 | if reqUrl.pathname !== '/login' 10 | .userfloat 11 | if currentAccount 12 | span.user.name 13 | = currentAccount.displayName 14 | if reqUrl.pathname !== '/account' 15 | a.nounder(href='/account') ▼ 16 | if reqUrl.pathname !== '/logout' 17 | form.lightweight( 18 | action=`/logout?cont=${encodeURIComponent(reqUrl.pathname + reqUrl.search)}` 19 | method="POST" 20 | name="logout") 21 | button.logoutlink(type="submit") logout 22 | else 23 | a.loginlink(href="/login") login 24 | +body() 25 | -------------------------------------------------------------------------------- /lib/handlers/login.pug: -------------------------------------------------------------------------------- 1 | mixin title() 2 | | Login 3 | 4 | mixin body() 5 | h1 6 | | Login 7 | if typeof email === 'string' && !/\S/.test(email) 8 | .error 9 | | Email is a required field. 10 | 11 | form(method="POST" action=reqUrl.pathname id="login") 12 | label(for="email") Email 13 | input(name="email" value=email) 14 | input(name="cont" type="hidden" value=cont) 15 | br 16 | | There is no password input since testing a credential store 17 | | is out of scope for this attack review, and requiring 18 | | credentials or using a federated service like oauth would 19 | | complicate running locally and testing as different users. 20 | br 21 | button(type="submit" form="login") Login 22 | | 23 | | 24 | a(href=cont) 25 | button(type="button") Cancel 26 | 27 | include includes/envelope.pug 28 | -------------------------------------------------------------------------------- /test/main-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | 'use strict'; 19 | 20 | /** 21 | * @fileoverview 22 | * A "test" file that simply requires the main module, so that when 23 | * scripts/generate-production-source-list.js runs, there is a node in 24 | * the require graph from which it can start enumerating production modules. 25 | */ 26 | 27 | require('../main'); 28 | -------------------------------------------------------------------------------- /lib/handlers/four-oh-four.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | 'use strict'; 19 | 20 | /** 21 | * @fileoverview 22 | * Dumps an error trace to the output. 23 | */ 24 | 25 | const template = require('./four-oh-four.pug'); 26 | 27 | exports.handle = (bundle) => { 28 | const { res } = bundle; 29 | res.statusCode = 404; 30 | res.write(template(bundle)); 31 | res.end(); 32 | }; 33 | -------------------------------------------------------------------------------- /scripts/gen-vulnerable-patch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Builds a variant of the target server but with protective measures disabled. 4 | 5 | set -e 6 | 7 | # This should match the same lines in build-vulnerable.sh 8 | source_files="$( 9 | git check-ignore -n -v --no-index \ 10 | $( find lib -type f | grep -v lib/framework; 11 | echo package.json main.js scripts/run-locally.js static/* ) \ 12 | | perl -ne 'print "$1\n" if m/^::\t(.*)/' | sort 13 | )" 14 | 15 | ( 16 | for f in $source_files node_modules/pug-require/index.js; do 17 | diff -u "$f" vulnerable/"$f" || true 18 | done 19 | ) | perl -pe 's/^((?:\+\+\+|---) .*)\t\d{4}-\d\d?-\d\d? \d\d?:\d\d?:\d\d?[.]\d+ [+\-]\d+$/$1\t2000-01-01 12:00:00.000000000 -0500/' \ 20 | > vulnerable.patch 21 | 22 | # This should match the same lines in build-vulnerable.sh 23 | hash_from="$( 24 | shasum -ba 1 vulnerable.patch $source_files 25 | )" 26 | 27 | if [ -d vulnerable/ ]; then 28 | echo -n "$hash_from" > vulnerable/patch-base.sha1 29 | fi 30 | -------------------------------------------------------------------------------- /lib/framework/is-prod.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | 'use strict'; 19 | 20 | /** 21 | * @fileoverview 22 | * Exports isProduction:true when the environment indicates that the system is running in 23 | * production. 24 | * https://expressjs.com/en/advanced/best-practice-performance.html#set-node_env-to-production 25 | */ 26 | 27 | // eslint-disable-next-line no-process-env 28 | module.exports.isProduction = process.env.NODE_ENV === 'production'; 29 | -------------------------------------------------------------------------------- /scripts/postinstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | set -x 5 | 6 | # Apply some patches to workaround issues. 7 | 8 | pushd "$(dirname "$(dirname "$0")")" > /dev/null 9 | PROJECT_ROOT="$PWD" 10 | popd > /dev/null 11 | 12 | [ -f "$PROJECT_ROOT"/package.json ] 13 | 14 | pushd "$PROJECT_ROOT" > /dev/null 15 | PATCHES_DIR="$PROJECT_ROOT/patches" 16 | if grep -q eval "$PROJECT_ROOT/node_modules/depd/index.js"; then 17 | # Update to (unreleased as of this writing) depd version 2.0.0 which 18 | # uses `new Function` instead of `eval` to create an arity-matching 19 | # function wrapper. 20 | # lib/framework/code-loading-function-proxy.js allows version 2.0.0 21 | # to run on a node runtime with --disallow_code_generation_from_strings 22 | # but eval-using version 1.1.2 cannot. 23 | # 24 | # The specific commit we need is 25 | # github.com/dougwilson/nodejs-depd/commit/887283b41c43d98b8ee8e8110d7443155de28682#diff-e1bbd4f15e3b63427b4261e05b948ea8 26 | patch -p1 < patches/node_modules-depd-index.patch 27 | fi 28 | if ! grep -q isExtensible "$PROJECT_ROOT/node_modules/fs-ext/fs-ext.js"; then 29 | patch -p1 < patches/node_modules-fsext-fsext.patch 30 | fi 31 | popd > /dev/null 32 | -------------------------------------------------------------------------------- /lib/handlers/client-error.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | 'use strict'; 19 | 20 | /** 21 | * @fileoverview 22 | * Receives errors from the client including CSP reports and telemetry from the browser console. 23 | */ 24 | 25 | exports.handle = (bundle) => { 26 | const { req, res, currentAccount } = bundle; 27 | 28 | const message = req.body.replace(/\r\n?|\n/g, '$& | '); 29 | 30 | // eslint-disable-next-line no-console 31 | console.log(`Client-error from ${ currentAccount ? currentAccount.aid : '' }: ${ message }`); 32 | 33 | res.statusCode = 200; 34 | res.end(); 35 | }; 36 | -------------------------------------------------------------------------------- /lib/handlers/error.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | 'use strict'; 19 | 20 | /** 21 | * @fileoverview 22 | * Dumps an error trace to the output. 23 | */ 24 | 25 | const { isProduction } = require('../framework/is-prod.js'); 26 | const template = require('./error.pug'); 27 | 28 | function handleError(bundle, exc) { 29 | const { res } = bundle; 30 | 31 | // eslint-disable-next-line no-console 32 | console.error(exc); 33 | 34 | res.statusCode = 500; 35 | res.write(template(Object.assign({}, bundle, { exc, isProduction }))); 36 | res.end(); 37 | } 38 | 39 | exports.handle = handleError; 40 | -------------------------------------------------------------------------------- /lib/framework/unprivileged-require.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | 'use strict'; 19 | 20 | /** 21 | * @fileoverview 22 | * Exports a require function that should not be granted any privilege. 23 | */ 24 | 25 | const logged = new Set(); 26 | 27 | module.exports = Object.freeze( 28 | // eslint-disable-next-line prefer-arrow-callback 29 | function unprivilegedRequire(x, who) { 30 | if (!logged.has(x)) { 31 | logged.add(x); 32 | // eslint-disable-next-line no-console 33 | console.trace(who); 34 | } 35 | // eslint-disable-next-line global-require 36 | return require(x); 37 | }); 38 | -------------------------------------------------------------------------------- /static/common.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview 3 | * Client side JavaScript. 4 | */ 5 | 6 | /* eslint-env browser */ 7 | /* eslint no-var: 0, no-console: 0, prefer-arrow-callback: 0, 8 | prefer-destructuring: 0, prefer-template: 0, prefer-rest-params: 0 */ 9 | 10 | 'use strict'; 11 | 12 | // Focus on the first form element. 13 | document.addEventListener('DOMContentLoaded', function onReady() { 14 | var form = document.querySelector('form:not([name="logout"])'); 15 | if (form) { 16 | var element = form.elements[0]; 17 | if (element && element.focus) { 18 | element.focus(); 19 | } 20 | } 21 | }); 22 | 23 | // Transmit client-side errors to the server logs. 24 | void (() => { 25 | var originalError = console.error; 26 | // eslint-disable-next-line id-blacklist 27 | console.error = function error() { 28 | var args = Array.prototype.slice.call(arguments); 29 | // Fire and forget error to server. 30 | var message = new window.XMLHttpRequest(); 31 | var url = (window.origin || document.origin) + '/client-error'; 32 | message.open('POST', url, true); 33 | message.setRequestHeader('Content-type', 'text/plain;charset=UTF-8'); 34 | message.send(args.join(' ')); 35 | return originalError.apply(console, args); 36 | }; 37 | })(); 38 | -------------------------------------------------------------------------------- /test/cases/end-to-end/static-case.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | 'use strict'; 19 | 20 | /* eslint array-element-newline: 0 */ 21 | 22 | const { URL } = require('url'); 23 | 24 | module.exports = { 25 | requests: (baseUrl) => [ 26 | { 27 | req: { 28 | url: new URL('/hello-world.html', baseUrl).href, 29 | method: 'GET', 30 | }, 31 | res: { 32 | statusCode: 200, 33 | body: [ 34 | '', 35 | 'Hello, World!', 36 | '', 37 | ], 38 | logs: { 39 | stderr: '', 40 | stdout: '', 41 | }, 42 | }, 43 | }, 44 | ], 45 | }; 46 | -------------------------------------------------------------------------------- /lib/framework/builtin-module-ids.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | 'use strict'; 19 | 20 | /** 21 | * @fileoverview 22 | * Exports a list of builtin modules, and also a convenient containment predicate. 23 | */ 24 | 25 | const { binding } = require('process'); 26 | 27 | const builtinModuleIds = 28 | Object.getOwnPropertyNames(binding('natives')) 29 | .filter(RegExp.prototype.test.bind(/^[^_][^/\\]+$/)); 30 | 31 | const builtinModuleIdSet = new Set(builtinModuleIds); 32 | 33 | builtinModuleIds.isBuiltinModuleId = function isBuiltinModuleId(moduleId) { 34 | return builtinModuleIdSet.has(moduleId); 35 | }; 36 | 37 | Object.freeze(builtinModuleIds); 38 | 39 | module.exports = builtinModuleIds; 40 | -------------------------------------------------------------------------------- /lib/handlers/account.pug: -------------------------------------------------------------------------------- 1 | //- SENSITIVE - Trusted not to leak personally identifying information 2 | //- relating to the current user. 3 | 4 | mixin title() 5 | | Edit Account 6 | 7 | mixin body() 8 | - let displayName = currentAccount.displayName || '' 9 | form(id="account" method="post" action="/account") 10 | table.formatting 11 | tr 12 | td(colspan=2) 13 | h1 Public Profile 14 | tr 15 | td 16 | label(for= "displayName") Display Name 17 | td 18 | input(name="displayName" value=displayName) 19 | input(name="displayNameIsHtml" checked=(typeof currentAccount.displayName === 'object') type="checkbox") 20 | label.assocleft(for= "displayNameIsHtml") HTML 21 | tr 22 | td 23 | label(for= "publicUrl") Public URL 24 | td 25 | input(name="publicUrl" value=(currentAccount.publicUrl || '')) 26 | tr 27 | td(colspan=2) 28 | h1 Private Profile 29 | tr 30 | td 31 | label(for= "realName") Real Name 32 | td 33 | input(name="realName" value=require.moduleKeys.unbox(currentAccount.realName, () => true)) 34 | tr 35 | td 36 | label(for= "email") Email 37 | td 38 | input(name="email" value=require.moduleKeys.unbox(currentAccount.email, () => true)) 39 | input(name="cont" type="hidden" value=cont) 40 | button(form="account" type="submit") Submit 41 | | 42 | | 43 | a(href=cont) 44 | button(type="button") Cancel 45 | br 46 | 47 | include includes/envelope.pug 48 | -------------------------------------------------------------------------------- /test/cases/end-to-end/client-error-case.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | 'use strict'; 19 | 20 | /* eslint array-element-newline: 0 */ 21 | 22 | 23 | const { URL } = require('url'); 24 | 25 | module.exports = { 26 | requests: (baseUrl) => [ 27 | { 28 | req: { 29 | uri: new URL('/client-error', baseUrl).href, 30 | method: 'POST', 31 | headers: { 32 | 'content-type': 'text/plain;charset=UTF-8', 33 | }, 34 | body: 'Something happened\n at foo.js\n at bar.js', 35 | // No CSRF token 36 | }, 37 | res: { 38 | body: 'IGNORE', 39 | logs: { 40 | stderr: '', 41 | stdout: ( 42 | 'POST /client-error\n' + 43 | 'Client-error from : Something happened\n' + 44 | ' | at foo.js\n' + 45 | ' | at bar.js\n' 46 | ), 47 | }, 48 | statusCode: 200, 49 | }, 50 | }, 51 | ], 52 | }; 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Application specific generated files 2 | # node binary generated at postinstall time 3 | /bin/node 4 | /bin/npm 5 | /generated 6 | # Patched clone of node git repo 7 | /bin/node.d/node 8 | # Directory owned by locally running postgres instances. 9 | # See script/run-locally.js 10 | /pg 11 | # Stores uploaded files 12 | /static/user-uploads 13 | # Server variant 14 | /vulnerable/** 15 | 16 | # Emacs droppings 17 | *~ 18 | 19 | # Logs 20 | logs 21 | *.log 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | 26 | # Runtime data 27 | pids 28 | *.pid 29 | *.seed 30 | *.pid.lock 31 | 32 | # Directory for instrumented libs generated by jscoverage/JSCover 33 | lib-cov 34 | 35 | # Coverage directory used by tools like istanbul 36 | coverage 37 | 38 | # nyc test coverage 39 | .nyc_output 40 | 41 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 42 | .grunt 43 | 44 | # Bower dependency directory (https://bower.io/) 45 | bower_components 46 | 47 | # node-waf configuration 48 | .lock-wscript 49 | 50 | # Compiled binary addons (https://nodejs.org/api/addons.html) 51 | build/Release 52 | 53 | # Dependency directories 54 | node_modules/ 55 | jspm_packages/ 56 | 57 | # TypeScript v1 declaration files 58 | typings/ 59 | 60 | # Optional npm cache directory 61 | .npm 62 | 63 | # Optional eslint cache 64 | .eslintcache 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variables file 76 | .env 77 | 78 | # next.js build output 79 | .next 80 | -------------------------------------------------------------------------------- /lib/handlers/includes/post-common.pug: -------------------------------------------------------------------------------- 1 | mixin post(post) 2 | if post.authorName 3 | span.author.name 4 | if post.authorUrl 5 | a(href=post.authorUrl) 6 | = post.authorName 7 | else 8 | = post.authorName 9 | if post.created 10 | span.created 11 | = fmtDate(post.created) 12 | div.body=(post.body || '') 13 | if post.images && post.images.length 14 | div.images 15 | each image in post.images 16 | a.usercontent(href=image) 17 | img(src=image) 18 | 19 | mixin post_form(preview) 20 | each error in (userErrors || []) 21 | p.error 22 | =error 23 | form(id="post-form" action="/post" enctype="multipart/form-data" method="POST") 24 | textarea(name="body" cols="40" rows="5")= ((preview && (preview.unsanitizedBody || preview.body)) || '') 25 | div 26 | label(for="public") Public 27 | input(type="checkbox" id="public" name="public" checked=preview.isPublic value="1") 28 | div 29 | each image, index in preview.images 30 | .imagepreview 31 | input(id=`image-${ index }` type="checkbox" checked name="imagepath" value=image) 32 | label(for=`image-${ index }`) 33 | img.usercontent(src=image) 34 | br 35 | input(type="file" name="upload" multiple="multiple") 36 | if now 37 | input(type="hidden" name="now" value=+now) 38 | hr 39 | button(type="submit" name="preview" value="1" form="post-form") Preview 40 | | 41 | | 42 | if preview 43 | button(type="submit" form="post-form") Post 44 | | 45 | | 46 | a(href='/') 47 | button(type="button") Cancel 48 | -------------------------------------------------------------------------------- /test/run-hook.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | 'use strict'; 19 | 20 | const path = require('path'); 21 | const { interceptStderr, interceptStdout } = require('capture-console'); 22 | 23 | function runHook(hook, source, target) { 24 | const sourceId = module.id.replace(/[^/]+\/[^/]+$/, source); 25 | const sourceFile = path.join(__dirname, source); 26 | 27 | if (typeof hook.flush === 'function') { 28 | hook.flush(); 29 | } 30 | 31 | const sentinel = {}; 32 | let thrown = sentinel; 33 | let result = null; 34 | let stdout = null; 35 | const stderr = interceptStderr(() => { 36 | stdout = interceptStdout(() => { 37 | try { 38 | result = hook(sourceFile, sourceId, target, require.resolve); 39 | } catch (exc) { 40 | thrown = exc; 41 | } finally { 42 | if (typeof hook.flush === 'function') { 43 | hook.flush(); 44 | } 45 | } 46 | }); 47 | }); 48 | if (thrown !== sentinel) { 49 | throw thrown; 50 | } 51 | return { result, stderr, stdout }; 52 | } 53 | 54 | module.exports = { runHook }; 55 | -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # Make a directory for the production source list 6 | mkdir -p generated 7 | 8 | # Generate the production source list 9 | # Run the tests once to make sure we have an up-to-date production source list, 10 | PROD_SOURCE_LIST= 11 | PROD_MASTER_HASH= 12 | if [ -f generated/prod-sources.json ]; then 13 | PROD_MASTER_HASH="$(shasum -a 256 generated/prod-sources.json)" 14 | fi 15 | if ! SOURCE_LIST_UP_TO_DATE=0 scripts/generate-production-source-list.js generated/prod-sources; then 16 | NEW_PROD_MASTER_HASH="$(shasum -a 256 generated/prod-sources.json)" 17 | if [[ "$PROD_MASTER_HASH" != "$NEW_PROD_MASTER_HASH" ]]; then 18 | echo Updated production source list 19 | echo . "$PROD_MASTER_HASH" '->' "$NEW_PROD_MASTER_HASH" 20 | echo Rerunning 21 | else 22 | exit 1 23 | fi 24 | fi 25 | # Rerun knowing that the source list is up-to-date. 26 | SOURCE_LIST_UP_TO_DATE=1 scripts/generate-production-source-list.js generated/prod-sources 27 | 28 | if [[ "$TRAVIS" != "true" ]]; then 29 | # Sanity check the server by starting it, and then tell it to hangup. 30 | echo Starting server 31 | ./scripts/run-locally.js & 32 | main_pid="$!" 33 | # Spawn a fire-and-forget subprocess to stop the server from listening forever. 34 | ( 35 | sleep 3 36 | kill -s INT "$main_pid" 37 | ) & 38 | hup_pid="$!" 39 | wait "$main_pid" # Wait for the server to exit 40 | server_result="$?" 41 | kill "$hup_pid" >& /dev/null || true 42 | if [[ "$server_result" != "0" ]]; then 43 | echo 'FAILED: Server smoke test' 44 | exit "$server_result" 45 | fi 46 | echo 'PASSED: Server smoke test' 47 | fi 48 | -------------------------------------------------------------------------------- /test/end-to-end-vulnerable-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | 'use strict'; 19 | 20 | /** 21 | * @fileoverview 22 | * Runs the same test suite as end-to-end-test, but in a separate process 23 | * and against the vulnerable variant of the server. 24 | */ 25 | 26 | const { describe } = require('mocha'); 27 | const path = require('path'); 28 | const process = require('process'); 29 | 30 | const runEndToEndCases = require('./end-to-end-common.js'); 31 | 32 | const externalProcessTestServer = require('./external-process-test-server.js'); 33 | const root = path.resolve(path.join(__dirname, '..', 'vulnerable')); 34 | 35 | // eslint-disable-next-line no-process-env 36 | if (process.env.SOURCE_LIST_UP_TO_DATE !== '0') { 37 | describe('end-to-end-vulnerable', () => { 38 | const options = { 39 | isVulnerable: true, 40 | // Vulnerable server hardcodes isProduction to true. 41 | isProduction: true, 42 | quiet: true, 43 | root, 44 | }; 45 | const externalProcessTest = externalProcessTestServer(root); 46 | runEndToEndCases(externalProcessTest, options); 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /lib/framework/well-formedness-check.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | /** 19 | * @fileoverview 20 | * Like performing `new Function(x)` to make sure that x is a valid JavaScript FunctionBody 21 | * and does not do anything like `}foo();(function(){` to break out of a function wrapper. 22 | */ 23 | 24 | 'use strict'; 25 | 26 | const { isArray } = Array; 27 | const { SyntaxError } = global; 28 | const { parse } = require('@babel/parser'); 29 | 30 | module.exports = (code, sourceFilename) => { 31 | const ast = parse(`(function(){${ code }})`, { sourceFilename }); 32 | if (ast.type === 'File') { 33 | const { program } = ast; 34 | if (program && program.type === 'Program') { 35 | const { body } = program; 36 | if (isArray(body) && body.length === 1) { 37 | const [ stmt ] = body; 38 | if (stmt.type === 'ExpressionStatement') { 39 | const { expression } = stmt; 40 | if (expression.type === 'FunctionExpression') { 41 | return; 42 | } 43 | } 44 | } 45 | } 46 | } 47 | throw new SyntaxError('Expected valid FunctionBody'); 48 | }; 49 | -------------------------------------------------------------------------------- /test/cases/end-to-end/no-such-file-case.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | 'use strict'; 19 | 20 | /* eslint array-element-newline: 0 */ 21 | 22 | const { URL } = require('url'); 23 | 24 | module.exports = { 25 | requests: (baseUrl) => [ 26 | { 27 | req: { 28 | uri: new URL('/no-such-file', baseUrl).href, 29 | }, 30 | res: { 31 | body: [ 32 | '', 33 | '', 34 | '', 35 | 'File Not Found:', 36 | '/no-such-file', 37 | '', 39 | '', 40 | '', 41 | '', 42 | '
', 43 | 'login', 44 | '
', 45 | '

404

', 46 | '

Oops! Nothing at', 47 | `${ baseUrl.origin }/no-such-file

`, 48 | '', 49 | '', 50 | ], 51 | logs: { 52 | stderr: '', 53 | stdout: 'GET /no-such-file\n', 54 | }, 55 | statusCode: 404, 56 | }, 57 | }, 58 | ], 59 | }; 60 | -------------------------------------------------------------------------------- /test/end-to-end-lockeddown-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | 'use strict'; 19 | 20 | /** 21 | * @fileoverview 22 | * Runs the same test suite as end-to-end-test, but in a separate process. 23 | * It helps to have both of these. 24 | * 25 | * end-to-end-test tests code coverage and builds the dynamic module graph 26 | * used by scripts/generate-production-source-list.js. 27 | * 28 | * This test checks that we get the same results even when the security 29 | * machinery under lib/framework is in production configuration. 30 | */ 31 | 32 | const { describe } = require('mocha'); 33 | const path = require('path'); 34 | const process = require('process'); 35 | 36 | const runEndToEndCases = require('./end-to-end-common.js'); 37 | 38 | const externalProcessTestServer = require('./external-process-test-server.js'); 39 | const root = path.resolve(path.join(__dirname, '..')); 40 | 41 | // eslint-disable-next-line no-process-env 42 | if (process.env.SOURCE_LIST_UP_TO_DATE !== '0' && !('TRAVIS' in process.env)) { 43 | describe('end-to-end-lockeddown', () => { 44 | const options = { 45 | // We pass NODE_ENV=production. 46 | isProduction: true, 47 | quiet: true, 48 | root, 49 | }; 50 | const externalProcessTest = externalProcessTestServer(root); 51 | runEndToEndCases(externalProcessTest, options); 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /lib/handlers/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | 'use strict'; 19 | 20 | /** 21 | * @fileoverview 22 | * Displays recent posts visible to the current user. 23 | */ 24 | 25 | const relativeDate = require('tiny-relative-date'); 26 | const template = require('./index.pug'); 27 | const { getPosts } = require('../dbi.js'); 28 | 29 | 30 | exports.handle = (bundle, handleError) => { 31 | const { res, reqUrl, database, currentAccount } = bundle; 32 | 33 | const aid = currentAccount ? currentAccount.aid : null; 34 | const viewAsPublicParam = reqUrl.searchParams.get('viewAsPublic'); 35 | const viewAsPublic = typeof viewAsPublicParam === 'string' && viewAsPublicParam !== 'false'; 36 | // Allow tests to specify "now" so that we can get repeatable test behavior. 37 | const now = new Date(Number(reqUrl.searchParams.get('now')) || Date.now()); 38 | const limit = Number(reqUrl.searchParams.get('count') || 0); 39 | const offset = Number(reqUrl.searchParams.get('offset') || 0); 40 | getPosts(database, viewAsPublic ? null : currentAccount, { limit, offset }).then( 41 | (posts) => { 42 | res.statusCode = 200; 43 | res.end(template(Object.assign( 44 | {}, 45 | bundle, 46 | { 47 | viewAsPublic: viewAsPublic && aid !== null, 48 | posts, 49 | fmtDate(date) { 50 | return relativeDate(date, now); 51 | }, 52 | }))); 53 | }, 54 | handleError); 55 | }; 56 | -------------------------------------------------------------------------------- /test/init-hooks-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | 'use strict'; 19 | 20 | const { expect } = require('chai'); 21 | const { describe, it } = require('mocha'); 22 | 23 | const hook = require('../lib/framework/init-hooks.js'); 24 | const { runHook } = require('./run-hook.js'); 25 | 26 | describe('init-hooks', () => { 27 | it('require child_process', () => { 28 | expect(runHook(hook, 'init-hooks-test.js', 'child_process')) 29 | .to.deep.equals({ 30 | result: require.resolve('../lib/framework/module-hooks/innocuous.js'), 31 | stderr: ( 32 | 'lib/framework/module-hooks/sensitive-module-hook.js:' + 33 | ' Blocking require("child_process") by test/init-hooks-test.js' + 34 | '\n\n\tUse safe/child_process.js instead.\n'), 35 | stdout: '', 36 | }); 37 | }); 38 | it('require package.json', () => { 39 | expect(runHook(hook, 'init-hooks-test.js', '../package.json')) 40 | .to.deep.equals({ 41 | result: '../package.json', 42 | stderr: '', 43 | stdout: '', 44 | }); 45 | }); 46 | it('doppelgangers', () => { 47 | let stringifyCount = 0; 48 | const doppelganger = { 49 | toString() { 50 | return [ '../package.json' ][stringifyCount++] || 'child_process'; 51 | }, 52 | }; 53 | 54 | expect(runHook(hook, 'init-hooks-test.js', doppelganger)) 55 | .to.deep.equals({ 56 | result: '../package.json', 57 | stderr: '', 58 | stdout: '', 59 | }); 60 | expect(stringifyCount).to.equal(1); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /test/cases/end-to-end/echo-case.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | 'use strict'; 19 | 20 | /* eslint array-element-newline: 0 */ 21 | 22 | const { URL } = require('url'); 23 | 24 | module.exports = { 25 | requests: (baseUrl, { isVulnerable }) => (isVulnerable ? [] : [ 26 | { 27 | req: { 28 | uri: new URL('/echo?a%22=b%27&foo=bar&baz', baseUrl).href, 29 | }, 30 | res: { 31 | body: [ 32 | '', 33 | '', 34 | '', 35 | 'Database Echo', 36 | '', 38 | '', 39 | '', 40 | '', 41 | '
', 42 | 'login', 43 | '
', 44 | '

Echo

', 45 | '', 46 | '', 47 | '', 48 | '', 49 | '', 50 | '', 51 | '', 52 | '', 53 | '', 54 | '', 56 | '', 57 | '
a"foobaz
b'bar', 55 | '
', 58 | '', 59 | '', 60 | ], 61 | logs: { 62 | stderr: '', 63 | stdout: ( 64 | 'GET /echo?a%22=b%27&foo=bar&baz\n' + 65 | 'echo sending SELECT e\'b\'\'\' AS "a""", \'bar\' AS "foo", \'\' AS "baz"\n'), 66 | }, 67 | statusCode: 200, 68 | }, 69 | }, 70 | ]), 71 | }; 72 | -------------------------------------------------------------------------------- /scripts/preinstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | set -x 5 | 6 | # Checks out a specific version of node, patches it, and builds it. 7 | # This version of node allows trusted require hooks to intercept 8 | # code loads, and attaches keys to every user module loaded. 9 | # 10 | # This does not hijack any ambient credentials. For reals! Well, probably not. 11 | 12 | pushd "$(dirname "$(dirname "$0")")" > /dev/null 13 | PROJECT_ROOT="$PWD" 14 | popd > /dev/null 15 | 16 | [ -f "$PROJECT_ROOT"/package.json ] 17 | 18 | # Don't bother building the runtime for Travis which can run tests on a stock runtime 19 | # docs.travis-ci.com/user/environment-variables/#default-environment-variables 20 | if [[ "true" != "$TRAVIS" ]]; then 21 | 22 | NODE_BUILD_PARENT="$PROJECT_ROOT/bin/node.d" 23 | NODE_BUILD_DIR="$NODE_BUILD_PARENT/node" 24 | 25 | if ! [ -x "$PROJECT_ROOT/bin/node" ]; then 26 | 27 | [ -d "$NODE_BUILD_PARENT" ] 28 | rm -rf "$NODE_BUILD_DIR" 29 | 30 | ( 31 | pushd "$NODE_BUILD_PARENT" 32 | git clone https://github.com/nodejs/node.git node 33 | pushd "$NODE_BUILD_DIR" 34 | # 2019-04-11, Version 11.14.0 35 | git reset --hard cd026f8226e18b8b0f1e1629353b366187a96b42 36 | patch -p1 < "$NODE_BUILD_PARENT/node.patch" 37 | ./configure 38 | make -j4 39 | popd 40 | popd 41 | ) 42 | 43 | cp "$NODE_BUILD_DIR"/node "$PROJECT_ROOT"/bin/ 44 | fi 45 | 46 | [ -x "$PROJECT_ROOT/bin/node" ] 47 | 48 | if ! [ -x "$PROJECT_ROOT/bin/npm" ]; then 49 | rm -f "$PROJECT_ROOT"/bin/npm 50 | 51 | # Pack npm 52 | pushd "$NODE_BUILD_DIR/deps/npm" 53 | npm install 54 | bin/npm-cli.js install 55 | NPM_TARBALL="$(bin/npm-cli.js pack | tail -1)" 56 | popd 57 | 58 | [ -n "$NPM_TARBALL" ] 59 | 60 | # Install an NPM that uses the built node locally. 61 | "$NODE_BUILD_DIR/deps/npm/bin/npm-cli.js" install --no-save "$NODE_BUILD_DIR/deps/npm/$NPM_TARBALL" 62 | ln -s "$PROJECT_ROOT/node_modules/.bin/npm" bin/npm 63 | fi 64 | 65 | [ -x "$PROJECT_ROOT/bin/npm" ] 66 | 67 | # Sanity check our patched version of node. 68 | [[ "true" == "$("$PROJECT_ROOT/bin/node" -e 'console.log(typeof require.moduleKeys.publicKey === `function`)')" ]] 69 | fi 70 | -------------------------------------------------------------------------------- /lib/handlers/echo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | 'use strict'; 19 | 20 | /** 21 | * @fileoverview 22 | * Assuming you've started the server at localhost:8080, going to 23 | * http://localhost:8080/echo?a=foo&b=bar&c=baz will send a request like 24 | * `SELECT ('foo' AS "a", 'bar' AS "b", 'baz' AS "c")` to the database. 25 | * 26 | * You can use this to easily test out some safesql bypasses. 27 | * Feel free to hack the structure here to try out other hacks 28 | */ 29 | 30 | const template = require('./echo.pug'); 31 | const safesql = require('safesql'); 32 | 33 | exports.handle = (bundle, handleError) => { 34 | const { database, reqUrl, res } = bundle; 35 | const inputs = Array.from(reqUrl.searchParams.entries()); 36 | if (!inputs.length) { 37 | // Make sure just visiting /echo doesn't barf with a SQL error. 38 | inputs.push([ 'Hello', 'World' ]); 39 | } 40 | 41 | function finish(failure, result) { 42 | if (failure) { 43 | handleError(failure); 44 | } else { 45 | res.statusCode = 200; 46 | res.end(template(Object.assign({}, bundle, { result }))); 47 | } 48 | } 49 | 50 | database.connect().then( 51 | (client) => { 52 | const query = safesql.pg`SELECT ${ 53 | inputs.map(([ name, value ]) => safesql.pg`${ value } AS "${ name }"`) }`; 54 | // eslint-disable-next-line no-console 55 | console.log(`echo sending ${ query }`); 56 | client.query(query).then( 57 | (resultSet) => { 58 | client.release(); 59 | finish(null, Object.entries(resultSet.rows[0])); 60 | }, 61 | (exc) => { 62 | client.release(); 63 | finish(exc, null); 64 | }); 65 | }, 66 | (exc) => { 67 | finish(exc, null); 68 | }); 69 | }; 70 | -------------------------------------------------------------------------------- /lib/safe/pg.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | 'use strict'; 19 | 20 | /** 21 | * @fileoverview 22 | * Modifies the postgres library to intercept calls to query and check that the 23 | * query string is a SafeSQL fragment. 24 | * 25 | * SqlFragments are produced by the safesql library, so by ensuring that pg 26 | * checks for SqlFragments and using node-sec-patterns to control which modules 27 | * we can bound the amount of code that might be at fault in a SQL injection. 28 | */ 29 | 30 | // TODO: This is destructive to pg. 31 | // Ask AJVincent / Tom Van Cutsem if we can just use 32 | // https://github.com/ajvincent/es-membrane to non-destructively intercept 33 | // calls to Query and maybe to guard access to Client.queryQueue and internal 34 | // methods. 35 | 36 | // SENSITIVE - Trusted to mutate database tables. 37 | // GUARANTEE - Exports an API that is a subset of that provided by package pg 38 | // but which only issues messages to the database whose content is part of a 39 | // SqlFragment minted by an authorized minter. 40 | 41 | const { apply } = Reflect; 42 | // eslint-disable-next-line id-length 43 | const pg = require('pg'); 44 | const { Mintable } = require('node-sec-patterns'); 45 | const { SqlFragment } = require('safesql'); 46 | 47 | const isSqlFragment = Mintable.verifierFor(SqlFragment); 48 | 49 | const { query: originalQuery } = pg.Client.prototype; 50 | pg.Client.prototype.query = function query(...argumentList) { 51 | const [ argument0 ] = argumentList; 52 | if (!isSqlFragment(argument0)) { 53 | throw new TypeError('Expected SqlFragment'); 54 | } 55 | argumentList[0] = argument0.content; 56 | return apply(originalQuery, this, argumentList); 57 | }; 58 | // Deny direct access to Query. 59 | pg.Query = null; 60 | 61 | module.exports = pg; 62 | -------------------------------------------------------------------------------- /lib/handlers/logout.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | 'use strict'; 19 | 20 | /** 21 | * @fileoverview 22 | * Remove the association between the current session and any account. 23 | */ 24 | 25 | require('module-keys/cjs').polyfill(module, require); 26 | 27 | const { URL } = require('url'); 28 | 29 | const safesql = require('safesql'); 30 | const template = require('./logout.pug'); 31 | 32 | // eslint-disable-next-line no-magic-numbers 33 | const STATUS_TEMPORARY_REDIRECT = 302; 34 | 35 | exports.handle = (bundle, handleError) => { 36 | const { res, req, reqUrl, database, sessionNonce } = bundle; 37 | 38 | let contUrl = null; 39 | { 40 | const cont = reqUrl.searchParams.get('cont') || req.headers.referer || '/'; 41 | contUrl = new URL(cont, reqUrl); 42 | if (contUrl.origin !== reqUrl.origin) { 43 | contUrl = new URL('/login', reqUrl); 44 | } 45 | } 46 | 47 | if (req.method !== 'POST') { 48 | // Can't do something non-idemopotent on GET. 49 | // Display a logout button. 50 | res.statusCode = 200; 51 | res.end(template(Object.assign({}, bundle, { cont: contUrl.href }))); 52 | return; 53 | } 54 | 55 | const sessionNonceValue = require.moduleKeys.unbox(sessionNonce, () => true); 56 | 57 | database.connect().then( 58 | (client) => { 59 | client.query(safesql.pg`UPDATE SESSIONS SET aid=NULL WHERE sessionnonce=${ sessionNonceValue }`) 60 | .then( 61 | () => { 62 | client.release(); 63 | res.statusCode = STATUS_TEMPORARY_REDIRECT; 64 | res.setHeader('Location', contUrl.href); 65 | res.end(); 66 | }, 67 | (exc) => { 68 | client.release(); 69 | handleError(exc); 70 | }); 71 | }, 72 | handleError); 73 | }; 74 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/breach.md: -------------------------------------------------------------------------------- 1 | # Breach Report 2 | 3 | *Thanks for participating!* 4 | 5 | *Text like this in \*italics\* explains how to write a good report and 6 | can be deleted. It may be easier to read these instructions in the 7 | "Preview" tab* 8 | 9 | *WARNING: GitHub's issue tracker does not save drafts. You can 10 | draft a report as a [secret gist][] which lets you save progress. 11 | We're equally happy to receive just a link to a well-written Gist 12 | so there's no need to copy/paste from your Gist here.* 13 | 14 | 15 | ## Security Property Compromised 16 | 17 | *Please explain which security property you compromised.* 18 | 19 | *Links to code are awesome!* 20 | 21 | *Clicking on a line in a Github source view and then clicking on "…" 22 | should offer you a "Copy permalink" option. See also [GitHub permalink][]. 23 | Putting a permalink on its own line will make it show up nicely in the issue 24 | report.* 25 | 26 | 27 | ## Reproducing 28 | 29 | *Ideally, we'd get a script that we can that attacks localhost:8080. 30 | (We will not respond kindly if the script does `rm -rf /` when we run 31 | it locally. Attacking others' hardware is out of bounds.)* 32 | 33 | *The [end-to-end tests][] show one way to craft an attack. Submitting 34 | another end-to-end test that fails but would pass if the target were not 35 | vulnerable is a great way to make it easy to verify your breach.* 36 | 37 | *If you can't boil down the attack to a script, then please provide 38 | a step-by-step rundown of what you did. The issue tracker allows you 39 | to upload screenshots or a screencast.* 40 | 41 | 42 | ## What should have happened differently? 43 | 44 | *Feel free to skip this section if it's obvious from the above, but if 45 | you provided an automated script, thanks, but please also explain what 46 | we should be looking for.* 47 | 48 | 49 | ## How could this have been prevented? 50 | 51 | *The person who identifies a problem is not responsible for fixing it, 52 | so this section is optional.* 53 | 54 | *If you do have any thoughts on how to fix the underlying problem, 55 | we'd love to hear them.* 56 | 57 | ---- 58 | 59 | *Thanks again for participating!* 60 | 61 | [GitHub permalink]: https://help.github.com/articles/creating-a-permanent-link-to-a-code-snippet/ 62 | [secret gist]: https://help.github.com/articles/about-gists/#secret-gists 63 | [end-to-end tests]: https://github.com/mikesamuel/attack-review-testbed/tree/master/test/cases/end-to-end 64 | -------------------------------------------------------------------------------- /test/code-loading-function-proxy-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | /* eslint no-magic-numbers: 0 */ 19 | 20 | 'use strict'; 21 | 22 | const { expect } = require('chai'); 23 | const { describe, it } = require('mocha'); 24 | const Fun = require('../lib/framework/code-loading-function-proxy.js')(require); 25 | 26 | describe('code-loading-function-proxy', () => { 27 | it('empty', () => { 28 | expect(() => Fun()).to.not.throw(); 29 | expect(() => Fun('')).to.not.throw(); 30 | }); 31 | it('typeof', () => { 32 | expect(() => Fun()).to.not.throw(); 33 | expect(() => Fun('')).to.not.throw(); 34 | }); 35 | it('consts', () => { 36 | expect(Fun('return 1 + 1')()).to.equal(2); 37 | expect(new Fun('return 1 + 1')()).to.equal(2); 38 | }); 39 | it('noreturn', () => { 40 | expect(Fun('1 + 1')()).to.equal(void 0); 41 | }); 42 | it('params', () => { 43 | expect(Fun('a', 'b', 'return a + b')('foo', 'bar')).to.equal('foobar'); 44 | expect(Fun('x', 'y', 'return x + y')(3, 4)).to.equal(7); 45 | }); 46 | it('invalid body', () => { 47 | expect(() => Fun('}, function () {')).to.throw(SyntaxError); 48 | }); 49 | it('invalid params', () => { 50 | expect(() => Fun('', '')).to.throw(SyntaxError); 51 | expect(() => Fun('1', '')).to.throw(SyntaxError); 52 | expect(() => Fun('1a', '')).to.throw(SyntaxError); 53 | expect(() => Fun('a-b', '')).to.throw(SyntaxError); 54 | expect(() => Fun('return', '')).to.throw(); 55 | }); 56 | it('flexible params', () => { 57 | expect( 58 | Fun( 59 | '_a', 'b0', '\\u0041', '\u03b1', 60 | 'return _a + b0 + \u0041 + \u03b1' 61 | )(1, 2, 3, 4)) 62 | .to.equal(10); 63 | }); 64 | it('nested', () => { 65 | expect(Fun('return new Function("return 1 + 1")()')()).to.equal(2); 66 | }); 67 | it('this', () => { 68 | const sentinel = {}; 69 | expect(Fun('return this').call(sentinel)).to.equal(sentinel); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /lib/safe/child_process.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | /** 19 | * @fileoverview 20 | * Wraps child_process to check shell strings. 21 | * 22 | * This provides a subset of an API equivalent to child_process, but where 23 | * command string, command, and argument values must be *ShFragment*s. 24 | * 25 | * The subset of the `child_process` API may be expanded as needed. 26 | */ 27 | 28 | // SENSITIVE - Trusted to spawn child processes. 29 | // GUARANTEE - Only allows execution of commands that were created by an 30 | // approved ShFragment minter. 31 | 32 | 'use strict'; // eslint-disable-line filenames/match-regex 33 | 34 | const childProcess = require('child_process'); 35 | const { ShFragment } = require('sh-template-tag'); 36 | const { Mintable } = require('node-sec-patterns'); 37 | const { inspect } = require('util'); 38 | 39 | const isShFragment = Mintable.verifierFor(ShFragment); 40 | 41 | module.exports.exec = (...args) => { 42 | let [ command ] = args; 43 | 44 | if (!isShFragment(command)) { 45 | throw new TypeError(`exec expected an ShFragment, not ${ inspect(command) } 46 | Maybe use require('sh-template-tag').sh\`...\` to create a command string.`); 47 | } 48 | command = command.content; 49 | 50 | let options = null; 51 | let onExit = null; 52 | 53 | switch (args.length) { 54 | case 1: 55 | break; 56 | case 2: { 57 | const [ , arg ] = args; 58 | if (arg) { 59 | if (typeof arg === 'function') { 60 | onExit = arg; 61 | } else if (typeof args[1] === 'object') { 62 | options = arg; 63 | } else { 64 | throw new Error('invalid argument'); 65 | } 66 | } 67 | break; 68 | } 69 | case 3: // eslint-disable-line no-magic-numbers 70 | [ , options, onExit ] = args; 71 | break; 72 | default: 73 | throw new Error('wrong number of arguments'); 74 | } 75 | 76 | options = options || {}; 77 | 78 | if (onExit) { 79 | return childProcess.exec(command, options, onExit); 80 | } 81 | return childProcess.exec(command, options); 82 | }; 83 | -------------------------------------------------------------------------------- /lib/framework/module-hooks/innocuous.js: -------------------------------------------------------------------------------- 1 | /** @license 2 | Copyright 2018 Google, Inc. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | /** 20 | * @fileoverview 21 | * An innocuous module that is substituted for blocked requires. 22 | */ 23 | 24 | function trap(kind, details) { 25 | // eslint-disable-next-line no-console 26 | console.warn(`banned module: trapped ${ kind } ${ JSON.stringify(details) }`); 27 | return void 0; 28 | } 29 | 30 | // Gather telemetry on what the requestor is doing. 31 | const handlers = Object.assign( 32 | Object.create(null), 33 | { 34 | apply(target, thisArg, args) { 35 | return trap('apply', args); 36 | }, 37 | construct(target, args) { 38 | return trap('construct', args); 39 | }, 40 | defineProperty(target, key, descriptor) { 41 | return trap('defineProperty', [ key, descriptor ]); 42 | }, 43 | deleteProperty(target, key) { 44 | return trap('deleteProperty', [ key ]) || false; 45 | }, 46 | // eslint-disable-next-line no-unused-vars 47 | get(target, key, receiver) { 48 | return trap('get', [ key ]); 49 | }, 50 | getOwnPropertyDescriptor(target, key) { 51 | return trap('getOwnPropertyDescriptor', [ key ]); 52 | }, 53 | // eslint-disable-next-line no-unused-vars 54 | getPrototypeOf(target) { 55 | return trap('getPrototypeOf', []) || null; 56 | }, 57 | has(target, key) { 58 | return trap('has', [ key ]) || false; 59 | }, 60 | isExtensible(target) { 61 | return trap('isExtensible', target) || false; 62 | }, 63 | // eslint-disable-next-line no-unused-vars 64 | ownKeys(target) { 65 | return trap('ownKeys', []) || []; 66 | }, 67 | // eslint-disable-next-line no-unused-vars 68 | preventExtensions(target) { 69 | return trap('preventExtensions', []) || false; 70 | }, 71 | // eslint-disable-next-line no-unused-vars 72 | set(target, key, value) { 73 | return trap('set', []) || false; 74 | }, 75 | // eslint-disable-next-line no-unused-vars 76 | setPrototypeOf(target, proto) { 77 | return trap('setPrototypeOf', []) || false; 78 | }, 79 | }); 80 | 81 | module.exports = new Proxy({}, handlers); 82 | -------------------------------------------------------------------------------- /test/lockdown-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | 'use strict'; 19 | 20 | const { expect } = require('chai'); 21 | const { describe, it } = require('mocha'); 22 | const lockdown = require('../lib/framework/lockdown.js'); 23 | 24 | function withSubst(rootObj, properties, substitute, blockFn) { 25 | let obj = rootObj; 26 | const { length } = properties; 27 | const lastProperty = properties[length - 1]; 28 | for (let i = 0; i < length - 1; ++i) { 29 | obj = obj[properties[i]]; 30 | } 31 | const original = obj[lastProperty]; 32 | try { 33 | obj[lastProperty] = substitute; 34 | } catch (exc) { 35 | return blockFn(); 36 | } 37 | try { 38 | return blockFn(); 39 | } finally { 40 | obj[lastProperty] = original; 41 | } 42 | } 43 | 44 | function noop() { 45 | // This block left intentionally blank. 46 | } 47 | 48 | describe('lockdown', () => { 49 | lockdown(); 50 | 51 | it('reliable path to Function.prototype.call', () => { 52 | expect( 53 | withSubst( 54 | Function, [ 'prototype', 'call' ], noop, 55 | () => { 56 | let callCount = 0; 57 | function fun() { 58 | ++callCount; 59 | } 60 | fun.call(); 61 | return callCount; 62 | })) 63 | .to.equal(1); 64 | }); 65 | it('arrays still mutable', () => { 66 | // eslint-disable-next-line no-magic-numbers 67 | const arr = [ 0, 1, 2, 3, 4, 5 ]; 68 | arr.length = 3; 69 | expect(arr).to.deep.equals([ 0, 1, 2 ]); 70 | }); 71 | it('String replace', () => { 72 | expect( 73 | withSubst( 74 | String, [ 'prototype', 'replace' ], () => 'foo', 75 | () => 'bar'.replace(/[a-z]/g, '$&.'))) 76 | .to.equal('b.a.r.'); 77 | }); 78 | it('process not entirely unchecked', () => { 79 | expect(global.process instanceof Object).to.equal(true); 80 | 81 | const unprivilegedRequireModule = require.cache[require.resolve( 82 | '../lib/framework/unprivileged-require.js')]; 83 | expect(unprivilegedRequireModule.parent.id).to.equal( 84 | require.resolve('../lib/framework/delicate-globals-rewrite.js')); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /lib/safe/html.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | /** 19 | * @fileoverview 20 | * HTML utilities. 21 | */ 22 | 23 | // SENSITIVE - Trusted to mint TrustedHTML contract values. 24 | // GUARANTEE - sanitize mints only TrustedHTML that, when parsed by a modern 25 | // browser as the content of a
, , , or other common HTML element 26 | // in the context of a document that does not have CSP, X-XSS-Protection or other 27 | // mitigations enabled would not allow: 28 | // * attacker-controlled script execution 29 | // * navigation of the embedding window or any of its ancestor 30 | // windows to another page without user interaction 31 | // * forms generated by server code to send to an origin chosen by 32 | // an attacker, a la `
`. 34 | // * trusted path violation where attacker controlled content 35 | // appears outside a visual container for third-party content so 36 | // could confuse users who assume links in a place usually 37 | // reserved for Login links are first-party content. 38 | // * prevent display of lexically following content like Terms of 39 | // Service and other legalese. 40 | 41 | 'use strict'; 42 | 43 | require('module-keys/cjs').polyfill(module, require); 44 | 45 | const { TrustedHTML } = require('web-contract-types'); 46 | const { Mintable } = require('node-sec-patterns'); 47 | const createDOMPurify = require('dompurify'); 48 | const { JSDOM } = require('jsdom'); 49 | 50 | const mintTrustedHTML = require.moduleKeys.unbox( 51 | Mintable.minterFor(TrustedHTML), () => true, (x) => x); 52 | const { window } = (new JSDOM('')); 53 | const DOMPurify = createDOMPurify(window); 54 | const sanitize = DOMPurify.sanitize.bind(DOMPurify); 55 | 56 | /** 57 | * Sanitizes an arbitrary untrusted input to TrustedHTML by stripping 58 | * unsafe tags, attributes, and other constructs and normalizing the 59 | * output to avoid parser corner cases. 60 | * 61 | * @param {*} html Passes TrustedHTML through unchange. 62 | * Otherwises coerces to string and sanitizes. 63 | * @return {TrustedHTML} 64 | */ 65 | module.exports.sanitize = (html) => { 66 | if (TrustedHTML.is(html)) { 67 | return html; 68 | } 69 | return mintTrustedHTML(sanitize(`${ html }`)); 70 | }; 71 | -------------------------------------------------------------------------------- /static/styles.css: -------------------------------------------------------------------------------- 1 | body { font-family: Arial } 2 | 3 | body:after { font-size: 25%; content: "Terms of Service: Code is intentionaly insecure. Don't post anything sensitive!" } 4 | 5 | h1 { 6 | font-size: 125%; 7 | border-left: .25em solid #66b9bf; 8 | padding-left: .25em; 9 | } 10 | 11 | h1, label, table.echo > tbody > tr > th { 12 | color: #07889b; 13 | } 14 | 15 | ol.posts { 16 | padding: 0; 17 | } 18 | ol.posts > li { 19 | display: block; 20 | list-style: none; 21 | margin-bottom: .5em; 22 | } 23 | ol.posts > li > .author.name { 24 | font-size: 50%; 25 | font-style: italic; 26 | } 27 | ol.posts > li > .author.name:after { content: " said " } 28 | ol.posts > li > .created { 29 | font-size: 50%; 30 | font-style: italic; 31 | } 32 | ol.posts > li > .body { 33 | border-left: .25em solid #66b9bf; 34 | padding-left: .25em; 35 | margin-left: .25em; 36 | } 37 | 38 | table.echo { 39 | border-spacing: 0; 40 | border-collapse: collapse; 41 | } 42 | table.echo > tbody > tr > th { 43 | border: 2px solid #66b9bf; 44 | border: 2px solid #66b9bf; 45 | border: 2px solid #66b9bf; 46 | } 47 | table.echo > tbody > tr > td, 48 | table.echo > tbody > tr > th { 49 | padding: 2px; 50 | margin: 0; 51 | } 52 | 53 | table.echo > tbody > tr > td { 54 | border-left: 2px solid #bbb; 55 | border-right: 2px solid #bbb; 56 | border-bottom: 2px solid #bbb; 57 | } 58 | 59 | .userfloat { 60 | position: absolute; 61 | top: 2px; 62 | right: 2px; 63 | width: 10em; 64 | font-size: 50%; 65 | text-align: right; 66 | } 67 | 68 | .error { 69 | color: #800; 70 | font-style: italic; 71 | } 72 | 73 | label { 74 | font-size: 75%; 75 | font-weight: bold; 76 | padding-right: 1em; 77 | padding-left: 1em; 78 | } 79 | 80 | label.assocleft { 81 | padding-left: 0; 82 | } 83 | 84 | .userfloat > .user.name:after { content: ' : ' } 85 | 86 | form.lightweight { 87 | display: inline 88 | } 89 | 90 | button.logoutlink { 91 | font: inherit; 92 | color: inherit; 93 | border: none; 94 | background: none !important; 95 | padding: 0 !important; 96 | border-bottom:1px solid #444; 97 | cursor: pointer; 98 | } 99 | 100 | .imagepreview { 101 | border: 1px dotted #66b9bf; 102 | } 103 | 104 | .imagepreview, .imagepreview label, .imagepreview input[type="checkbox"] { 105 | vertical-align: middle; 106 | display: inline-block; 107 | margin: 2px; 108 | } 109 | 110 | img.usercontent, .usercontent img { 111 | max-width: 128px; 112 | max-height: 128px; 113 | } 114 | 115 | form#account > table.formatting input[type="text"], 116 | form#account > table.formatting input:not([type]) { 117 | width: 20em; 118 | } 119 | 120 | a.nounder { 121 | text-decoration: none; 122 | } 123 | 124 | .banner { 125 | border: 2px solid #07889b; 126 | background: #99ecf2; 127 | color: #07889b; 128 | width: 100%; 129 | text-align: center; 130 | display: block; 131 | margin-top: 1em; 132 | } 133 | 134 | .view-as-public:before { 135 | content: "Public View!" 136 | } 137 | -------------------------------------------------------------------------------- /test/poorly-written-linkifier-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | /* eslint no-magic-numbers: 0 */ 19 | 20 | 'use strict'; 21 | 22 | const { expect } = require('chai'); 23 | const { describe, it } = require('mocha'); 24 | const linkify = require('../lib/poorly-written-linkifier.js'); 25 | 26 | describe('poorly-written-linkifier', () => { 27 | it('empty', () => { 28 | expect(linkify('')).to.equal(''); 29 | }); 30 | it('no links', () => { 31 | expect(linkify('Hello, World!')).to.equal('Hello, World!'); 32 | }); 33 | it('internal url in tag', () => { 34 | expect(linkify('Hello, World!')) 35 | .to.equal('Hello, World!'); 36 | }); 37 | it('external url in tag', () => { 38 | expect(linkify('Hello, World!')) 39 | .to.equal('Hello, World!'); 40 | }); 41 | it('internal url outside tag', () => { 42 | expect(linkify('Hello, http://example.com/world!')) 43 | .to.equal('Hello, http://example.com/world!'); 44 | }); 45 | it('external url outside tag', () => { 46 | expect(linkify('Hello, http://other.com/world!')) 47 | .to.equal('Hello, http://other.com/world!'); 48 | }); 49 | it('internal url missing scheme', () => { 50 | expect(linkify('Hello, example.com/world!')) 51 | .to.equal('Hello, example.com/world!'); 52 | }); 53 | it('external url missing scheme', () => { 54 | expect(linkify('Hello, other.org/world!')) 55 | .to.equal('Hello, other.org/world!'); 56 | }); 57 | it('multiple', () => { 58 | expect(linkify('Go to
  • foo.com or
  • bar.com
')) 59 | .to.equal( 60 | 'Go to '); 62 | }); 63 | it('unfortunate', () => { 64 | // We want it to have a large attack surface. 65 | // eslint-disable-next-line no-script-url 66 | expect(linkify('4 lulz: javascript://example.com%0aalert(document.domain)')) 67 | // eslint-disable-next-line no-script-url 68 | .to.equal('4 lulz: ' + 69 | // eslint-disable-next-line no-script-url 70 | 'javascript://example.com%0aalert(document.domain)'); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /lib/framework/code-loading-function-proxy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | /** 19 | * @fileoverview 20 | * Provide a proxy that looks like `Function` and that, when used as a 21 | * constructor, behaves like `new Function` but without triggering V8's 22 | * allow_code_gen_callback. 23 | * 24 | * See lockdown.js for the code that introduces this code. 25 | */ 26 | 27 | 'use strict'; 28 | 29 | // SENSITIVE - Invokes vm to load code from strings. 30 | 31 | // eslint-disable-next-line no-use-before-define 32 | module.exports = makeProxy; 33 | 34 | const { Function } = global; 35 | 36 | const wellFormednessCheck = require('./well-formedness-check.js'); 37 | const paramPattern = /^(?:[_$A-Z\x80-\uFFFF]|\\u[0-9A-F]{4})(?:[_$0-9A-Z\x80-\uFFFF]|\\u[0-9A-F]{4})*$/i; 38 | const delicateGlobalsRewrite = require('./delicate-globals-rewrite.js'); 39 | 40 | function makeProxy(localRequire, filename) { 41 | // Use the requesters require function to give sensitive-module-hook 42 | // enough context to decide whether to allow access to vm. 43 | const { runInThisContext } = localRequire('vm'); 44 | 45 | const fun = new Proxy( 46 | Function, 47 | { 48 | apply(target, thisArg, argumentsList) { 49 | // eslint-disable-next-line no-use-before-define 50 | return loadCode(argumentsList); 51 | }, 52 | construct(target, argumentsList) { 53 | // eslint-disable-next-line no-use-before-define 54 | return loadCode(argumentsList); 55 | }, 56 | }); 57 | 58 | function loadCode(argumentList) { 59 | const formals = []; 60 | let body = ''; 61 | const nFormals = argumentList.length - 1; 62 | for (let i = 0; i < nFormals; ++i) { 63 | const paramName = `${ argumentList[i] }`; 64 | if (paramPattern.test(paramName)) { 65 | formals.push(paramName); 66 | } else { 67 | throw new SyntaxError(`Invalid param name: ${ paramName }`); 68 | } 69 | } 70 | body = (nFormals >= 0 && `${ argumentList[nFormals] }`) || ''; 71 | // new Function(code) 72 | // is an idiom used to check that code has no side-effects until 73 | // applied. 74 | wellFormednessCheck(body); 75 | 76 | const evalFilename = `eval:${ filename }`; 77 | 78 | const rewrittenBody = delicateGlobalsRewrite(body, evalFilename, localRequire); 79 | 80 | const code = `(function (${ formals.join(', ') }){ ${ rewrittenBody } })`; 81 | 82 | return runInThisContext( 83 | code, 84 | { 85 | filename: evalFilename, 86 | displayErrors: true, 87 | }); 88 | } 89 | 90 | return fun; 91 | } 92 | -------------------------------------------------------------------------------- /lib/framework/bootstrap-secure.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | 'use strict'; 19 | 20 | /** 21 | * @fileoverview 22 | * Coordinate startup of various pieces of security machinery. 23 | */ 24 | 25 | // SENSITIVE - Coordinates initialization of security machinery. 26 | 27 | // GUARANTEE - If the server is run with NODE_ENV=production 28 | // (see ../../test/end-to-end-lockeddown-test.js) then the 29 | // resource integrity and sensitive module hooks run on all 30 | // userland modules and uphold their guarantees. 31 | 32 | const process = require('process'); 33 | const path = require('path'); 34 | 35 | const { setInsecure } = require('./init-hooks.js'); 36 | const { isProduction } = require('./is-prod.js'); 37 | 38 | const nodeSecPatterns = require('node-sec-patterns'); 39 | const installModuleStubs = require('./module-stubs.js'); 40 | const lockdown = require('./lockdown.js'); 41 | 42 | module.exports = function bootstrap(projectRoot, isMain) { 43 | const insecureMode = !isProduction && 44 | // eslint-disable-next-line no-process-env 45 | process.env.UNSAFE_DISABLE_NODE_SEC_HOOKS === 'true'; 46 | 47 | // eslint-disable-next-line global-require 48 | const packageJson = require(path.join(projectRoot, 'package.json')); 49 | 50 | if (insecureMode) { 51 | // There will inevitably be a "disable because I'm developing" switch so 52 | // we ought test whether it's easy to workaround checks that dev features 53 | // aren't abusable in production. 54 | // eslint-disable-next-line no-console 55 | console.log('Running in insecure mode'); 56 | setInsecure(true); 57 | } else if (isMain) { 58 | // If not running as part of tests, then configure access 59 | // control checks to contract type constructors. 60 | nodeSecPatterns.authorize(packageJson, projectRoot); 61 | } 62 | 63 | // Visit files that are definitely needed but not loaded when running 64 | // generate-production-source-list.js. 65 | if (packageJson.mintable && packageJson.mintable.second) { 66 | for (const second of packageJson.mintable.second) { 67 | // eslint-disable-next-line global-require 68 | require(`${ second }/package.json`); 69 | } 70 | } 71 | 72 | // Install module stubs. 73 | installModuleStubs(isMain); 74 | // We don't want to install module stubs when running under 75 | // generate-production-source-list since that would mean that 76 | // the resource-integrity-hook rejects the access. 77 | 78 | // Lock down intrinsics early so security critical code can rely on properties 79 | // of objects that they create and which do not escape. 80 | lockdown(); 81 | }; 82 | -------------------------------------------------------------------------------- /scripts/build-vulnerable.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Builds a variant of the target server but with protective measures disabled. 4 | 5 | set -e 6 | 7 | # This needs to match the same lines in gen-vulnerable-patch.sh 8 | source_files="$( 9 | git check-ignore -n -v --no-index \ 10 | $( find lib -type f | grep -v lib/framework; 11 | echo package.json main.js scripts/run-locally.js static/* ) \ 12 | | perl -ne 'print "$1\n" if m/^::\t(.*)/' | sort 13 | )" 14 | hash_from="$( 15 | shasum -ba 1 vulnerable.patch $source_files 16 | )" 17 | 18 | 19 | force= 20 | check= 21 | while (( $# > 0 )); do 22 | case "$1" in 23 | -f) force=1; shift ;; 24 | -c) check=1; shift ;; 25 | esac 26 | done 27 | 28 | if [ -n "$check" ]; then 29 | if [ -f "vulnerable/patch-base.sha1" ] && [[ "$hash_from" != "$( cat vulnerable/patch-base.sha1 )" ]]; then 30 | echo \ 31 | "Vulnerable server needs repatching. 32 | If you tweaked the vulnerable server regenerate vulnerable.patch: 33 | ./scripts/gen-vulnerable-patch.sh 34 | else if you changed the target server, regenerate the vulnerable server thus: 35 | mv vulnerable vulnerable.bak 36 | ./scripts/build-vulnerable.sh 37 | " 38 | exit 1 39 | fi 40 | if [ -d vulnerable ]; then 41 | rejects="$(find vulnerable -name \*.rej | wc -l)" 42 | if (( $(find vulnerable -name \*.rej | wc -l) > 0 )); then 43 | echo "Vulnerable server patching failed with rejected chunks" 44 | find vulnerable -name \*.rej | perl -pe 's/^/\t* /' 45 | exit 1 46 | fi 47 | exit 0 48 | fi 49 | fi 50 | 51 | if [ -d vulnerable/ ]; then 52 | if [ -z "$force" ]; then 53 | echo "vulnerable/ directory already exists. Specify -f to force overwrite." 54 | exit 1 55 | fi 56 | rm -rf vulnerable/ 57 | fi 58 | 59 | echo Deleting old vulnerable/ 60 | rm -rf vulnerable/ 61 | 62 | echo Copying files over 63 | for f in $source_files; do 64 | 65 | mkdir -p vulnerable/"$(dirname "$f")" 66 | cp -r "$f" vulnerable/"$f" 67 | done 68 | 69 | rm -rf vulnerable/static/user-uploads 70 | 71 | echo Copying node_modules 72 | cp -r node_modules/ vulnerable/node_modules/ 73 | 74 | echo Patching 75 | pushd vulnerable/ >& /dev/null 76 | 77 | echo "#/bin/bash" > scripts/postinstall.sh 78 | chmod +x scripts/postinstall.sh 79 | 80 | echo \ 81 | '// The vulnerable target server is generated by a patch, so allow things like 82 | // commented out code and replacement of safesql.pg`...` with `...` 83 | module.exports = { 84 | "rules": { 85 | "line-comment-position": 0, 86 | "no-inline-comments": 0, 87 | "no-unreachable": 0, 88 | "padded-blocks": 0, 89 | "spaced-comment": 0, 90 | "quotes": 0 91 | } 92 | }; 93 | ' > .eslintrc.js 94 | 95 | for f in node_modules/{module-keys,node-sec-patterns,safesql,sh-template-tag,web-contract-types}/index.js \ 96 | node_modules/sh-template-tag/lib/tag.js node_modules/safesql/lib/id.js; do 97 | echo 'throw new Error(`'"$f"'! kapow!`);' > "$f" 98 | done 99 | 100 | patch -p0 < ../vulnerable.patch 101 | popd >& /dev/null 102 | 103 | echo -n "$hash_from" > vulnerable/patch-base.sha1 104 | -------------------------------------------------------------------------------- /patches/node_modules-depd-index.patch: -------------------------------------------------------------------------------- 1 | --- a/node_modules/depd/index.js 2018-01-11 23:59:13.000000000 -0500 2 | +++ b/node_modules/depd/index.js 2018-10-13 11:36:39.000000000 -0400 3 | @@ -1,6 +1,6 @@ 4 | /*! 5 | * depd 6 | - * Copyright(c) 2014-2017 Douglas Christopher Wilson 7 | + * Copyright(c) 2014-2018 Douglas Christopher Wilson 8 | * MIT Licensed 9 | */ 10 | 11 | @@ -8,8 +8,6 @@ 12 | * Module dependencies. 13 | */ 14 | 15 | -var callSiteToString = require('./lib/compat').callSiteToString 16 | -var eventListenerCount = require('./lib/compat').eventListenerCount 17 | var relative = require('path').relative 18 | 19 | /** 20 | @@ -92,7 +90,7 @@ 21 | } 22 | 23 | for (var i = 0; i < stack.length; i++) { 24 | - str += '\n at ' + callSiteToString(stack[i]) 25 | + str += '\n at ' + stack[i].toString() 26 | } 27 | 28 | return str 29 | @@ -129,6 +127,26 @@ 30 | } 31 | 32 | /** 33 | + * Determine if event emitter has listeners of a given type. 34 | + * 35 | + * The way to do this check is done three different ways in Node.js >= 0.8 36 | + * so this consolidates them into a minimal set using instance methods. 37 | + * 38 | + * @param {EventEmitter} emitter 39 | + * @param {string} type 40 | + * @returns {boolean} 41 | + * @private 42 | + */ 43 | + 44 | +function eehaslisteners (emitter, type) { 45 | + var count = typeof emitter.listenerCount !== 'function' 46 | + ? emitter.listeners(type).length 47 | + : emitter.listenerCount(type) 48 | + 49 | + return count > 0 50 | +} 51 | + 52 | +/** 53 | * Determine if namespace is ignored. 54 | */ 55 | 56 | @@ -167,7 +185,7 @@ 57 | */ 58 | 59 | function log (message, site) { 60 | - var haslisteners = eventListenerCount(process, 'deprecation') !== 0 61 | + var haslisteners = eehaslisteners(process, 'deprecation') 62 | 63 | // abort early if no destination 64 | if (!haslisteners && this._ignored) { 65 | @@ -310,7 +328,7 @@ 66 | // add stack trace 67 | if (this._traced) { 68 | for (var i = 0; i < stack.length; i++) { 69 | - formatted += '\n at ' + callSiteToString(stack[i]) 70 | + formatted += '\n at ' + stack[i].toString() 71 | } 72 | 73 | return formatted 74 | @@ -335,7 +353,7 @@ 75 | // add stack trace 76 | if (this._traced) { 77 | for (var i = 0; i < stack.length; i++) { 78 | - formatted += '\n \x1b[36mat ' + callSiteToString(stack[i]) + '\x1b[39m' // cyan 79 | + formatted += '\n \x1b[36mat ' + stack[i].toString() + '\x1b[39m' // cyan 80 | } 81 | 82 | return formatted 83 | @@ -400,18 +418,18 @@ 84 | } 85 | 86 | var args = createArgumentsString(fn.length) 87 | - var deprecate = this // eslint-disable-line no-unused-vars 88 | var stack = getStack() 89 | var site = callSiteLocation(stack[1]) 90 | 91 | site.name = fn.name 92 | 93 | - // eslint-disable-next-line no-eval 94 | - var deprecatedfn = eval('(function (' + args + ') {\n' + 95 | + // eslint-disable-next-line no-new-func 96 | + var deprecatedfn = new Function('fn', 'log', 'deprecate', 'message', 'site', 97 | '"use strict"\n' + 98 | + 'return function (' + args + ') {' + 99 | 'log.call(deprecate, message, site)\n' + 100 | 'return fn.apply(this, arguments)\n' + 101 | - '})') 102 | + '}')(fn, log, this, message, site) 103 | 104 | return deprecatedfn 105 | } 106 | -------------------------------------------------------------------------------- /test/collected-db-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | 'use strict'; 19 | 20 | const { after, before } = require('mocha'); 21 | const { spinUpDatabase } = require('../scripts/run-locally.js'); 22 | const dbTablesTest = require('./db-tables-test.js'); 23 | const endToEndTest = require('./end-to-end-test.js'); 24 | const { Pool } = require('../lib/safe/pg.js'); 25 | 26 | function noop() { 27 | // This function body left intentionally blank. 28 | } 29 | 30 | const testsThatRequireDb = [ dbTablesTest, endToEndTest ]; 31 | 32 | const { release, withDbConfig } = (() => { 33 | let pending = null; 34 | let dbConfig = null; 35 | let failure = null; 36 | let teardownFun = null; 37 | 38 | function scheduleToRun(...onDbAvailable) { 39 | for (const fun of onDbAvailable) { 40 | setTimeout(fun.bind(null, failure, dbConfig), 0); 41 | } 42 | } 43 | 44 | function maybeRunPending() { 45 | const toNotify = pending; 46 | pending = null; 47 | if (toNotify) { 48 | scheduleToRun(...toNotify); 49 | } 50 | } 51 | 52 | return { 53 | release() { 54 | // We don't need to wait for inflight database-using tasks to finish 55 | // before teardown since mocha describe() does that for us. 56 | setTimeout( 57 | () => { 58 | if (teardownFun) { 59 | teardownFun(); 60 | } 61 | }, 62 | // Wait a little bit. 63 | // TODO: Figure out why init-hooks-test fail with errors about 64 | // a connection closing hard due to the interrupting of the 65 | // DB server process. 66 | // ravis-ci.org/mikesamuel/attack-review-testbed/jobs/438005939#L528 67 | // eslint-disable-next-line no-magic-numbers 68 | 250); 69 | }, 70 | withDbConfig(onDbAvailable) { 71 | if (pending) { 72 | // Wait in line. 73 | pending.push(onDbAvailable); 74 | } else if (dbConfig) { 75 | scheduleToRun(onDbAvailable); 76 | } else { 77 | pending = [ onDbAvailable ]; 78 | try { 79 | spinUpDatabase(({ pgSocksDir, pgPort, teardown }) => { 80 | dbConfig = { 81 | user: 'webfe', 82 | host: pgSocksDir, 83 | port: pgPort, 84 | database: 'postgres', 85 | }; 86 | teardownFun = teardown; 87 | 88 | maybeRunPending(); 89 | }); 90 | } catch (exc) { 91 | teardownFun = noop; 92 | failure = exc; 93 | maybeRunPending(); 94 | } 95 | } 96 | }, 97 | }; 98 | })(); 99 | 100 | function makePoolPromise() { 101 | return new Promise((resolve, reject) => { 102 | withDbConfig( 103 | (dbConfigErr, dbConfig) => { 104 | if (dbConfigErr) { 105 | reject(dbConfigErr); 106 | } else { 107 | const pool = new Pool(dbConfig); 108 | /* 109 | pool.on('error', (err, client) => { 110 | console.error(`Unexpected error on idle client ${ client.creator }`, err); 111 | }); 112 | */ 113 | resolve(pool); 114 | } 115 | }); 116 | }); 117 | } 118 | 119 | for (let i = 0, { length } = testsThatRequireDb; i < length; ++i) { 120 | const test = testsThatRequireDb[i]; 121 | test(makePoolPromise); 122 | } 123 | 124 | before(() => withDbConfig(noop)); 125 | after(release); 126 | -------------------------------------------------------------------------------- /test/cases/end-to-end/index-case.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | 'use strict'; 19 | 20 | /* eslint array-element-newline: 0 */ 21 | 22 | const { URL } = require('url'); 23 | 24 | const now = Number(new Date('2019-04-12 12:00:00Z')); 25 | const homePagePath = `/?now=${ now }`; 26 | 27 | module.exports = { 28 | requests: (baseUrl, { isVulnerable }) => [ 29 | { 30 | req: { 31 | uri: new URL(homePagePath, baseUrl).href, 32 | method: 'GET', 33 | }, 34 | res: { 35 | body: [ 36 | '', 37 | '', 38 | '', 39 | 'Attack Review Testbed', 40 | '', 42 | '', 43 | '', 44 | '', 45 | '
', 46 | 'login', 47 | '
', 48 | '

Recent Posts

', 49 | '
    ', 50 | '
  1. ', 51 | 'Ada', 52 | 'a week ago', 53 | '
    Hi! My name is Ada. Nice to meet you!
    ', 54 | '
    ', 55 | '', 56 | '', 57 | '', 58 | '
    ', 59 | '
  2. ', 60 | '
  3. ', 61 | '', 62 | ( 63 | isVulnerable ? 64 | 'Bob' : 65 | 'Bob' 66 | ), 67 | '', 68 | '6 days ago', 69 | '
    Ada, !
    ', 70 | '
  4. ', 71 | '
  5. ', 72 | '', 73 | '', 74 | 'Deb', 75 | '', 76 | '5 days ago', 77 | '
    ', 78 | '¡Hi, all!', 79 | '
    ', 80 | '
  6. ', 81 | '
  7. ', 82 | '', 83 | '', 84 | 'Fae', 85 | '', 86 | '', 87 | '3 days ago', 88 | '
    Sigh! Yet another Facebook.com' + 89 | ' knockoff without any users.
    ', 90 | '
  8. ', 91 | '
  9. ', 92 | '', 93 | '', 94 | 'Fae', 95 | '', 96 | '', 97 | '2 days ago', 98 | '
    (It is probably insecure)
    ', 99 | '
  10. ', 100 | '
', 101 | '
', 102 | '', 103 | '', 104 | '', 105 | '
', 106 | '', 107 | '', 108 | ], 109 | logs: { 110 | stderr: '', 111 | stdout: `GET ${ homePagePath }\n`, 112 | }, 113 | statusCode: 200, 114 | }, 115 | }, 116 | ], 117 | }; 118 | -------------------------------------------------------------------------------- /lib/handlers/account.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | 'use strict'; 19 | 20 | /** 21 | * @fileoverview 22 | * Displays recent posts visible to the current user. 23 | */ 24 | 25 | require('module-keys/cjs').polyfill(module, require); 26 | 27 | const { URL } = require('url'); 28 | const template = require('./account.pug'); 29 | const safesql = require('safesql'); 30 | 31 | // eslint-disable-next-line no-magic-numbers 32 | const STATUS_TEMPORARY_REDIRECT = 302; 33 | 34 | function trimOrNull(str) { 35 | return (str && str.replace(/^\s+|\s+$/g, '')) || null; 36 | } 37 | 38 | exports.handle = (bundle, handleError) => { 39 | const { res, req, reqUrl, database, currentAccount } = bundle; 40 | 41 | // Logout before logging in 42 | if (!currentAccount) { 43 | res.statusCode = STATUS_TEMPORARY_REDIRECT; 44 | // Use continue to bounce back here after logging out. 45 | const dest = new URL(`/login?cont=${ encodeURIComponent(reqUrl.pathname + reqUrl.search) }`, reqUrl); 46 | res.setHeader('Location', dest.href); 47 | res.end(); 48 | return; 49 | } 50 | 51 | let params = null; 52 | let isPost = false; 53 | switch (req.method) { 54 | case 'GET': 55 | case 'HEAD': 56 | params = {}; 57 | for (const key of reqUrl.searchParams.keys()) { 58 | params[key] = reqUrl.searchParams.get(key); 59 | } 60 | break; 61 | case 'POST': 62 | isPost = true; 63 | params = req.body; 64 | break; 65 | default: 66 | handleError(new Error(req.method)); 67 | return; 68 | } 69 | 70 | const cont = (params.cont && params.cont[0]) || req.headers.referer || '/'; 71 | if (isPost) { 72 | commitThenRedirect(); // eslint-disable-line no-use-before-define 73 | } else { 74 | serveForm(); // eslint-disable-line no-use-before-define 75 | } 76 | 77 | function commitThenRedirect() { 78 | let contUrl = new URL(cont, reqUrl); 79 | if (contUrl.origin === reqUrl.origin) { 80 | contUrl = new URL('/', reqUrl); 81 | } 82 | 83 | const { aid } = currentAccount; 84 | const displayNameIsHtml = Boolean(params.displayNameIsHtml); 85 | const displayNameText = trimOrNull(params.displayName); 86 | const displayname = displayNameIsHtml ? null : displayNameText; 87 | const displaynamehtml = displayNameIsHtml ? displayNameText : null; 88 | const publicurl = trimOrNull(params.publicUrl); 89 | const realname = trimOrNull(params.realName); 90 | const email = trimOrNull(params.email); 91 | 92 | database.connect().then( 93 | (client) => { 94 | function releaseAndReject(exc) { 95 | client.release(); 96 | handleError(exc); 97 | } 98 | 99 | function success() { 100 | client.release(); 101 | res.statusCode = STATUS_TEMPORARY_REDIRECT; 102 | res.setHeader('Location', contUrl.href); 103 | res.end(); 104 | } 105 | 106 | function commitAccountChanges() { 107 | const sql = safesql.pg` 108 | UPDATE Accounts 109 | SET displayname=${ displayname }, 110 | displaynamehtml=${ displaynamehtml }, 111 | publicurl=${ publicurl } 112 | WHERE aid=${ aid }; 113 | 114 | UPDATE PersonalInfo 115 | SET realname=${ realname }, 116 | email=${ email } 117 | WHERE aid=${ aid };`; 118 | client.query(sql).then( 119 | success, 120 | releaseAndReject); 121 | } 122 | 123 | commitAccountChanges(); 124 | }, 125 | handleError); 126 | } 127 | 128 | function serveForm() { 129 | res.statusCode = 200; 130 | res.end(template(Object.assign({}, bundle, { cont }))); 131 | } 132 | }; 133 | -------------------------------------------------------------------------------- /lib/framework/init-hooks.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | 'use strict'; 19 | 20 | /** 21 | * @fileoverview 22 | * Loads and configures require hooks. 23 | */ 24 | 25 | // SENSITIVE - Defines a trusted require hook. 26 | 27 | // GUARANTEE - By controlling who edits ../../package.json, security 28 | // specialists can exercise oversight over which modules can load 29 | // sensitive modules. 30 | 31 | // The hook code below would be much more complicated if it could 32 | // reenter itself while unpacking its configuration. 33 | // Rather than trying to identify and workaround those complications 34 | // we use a monotonic state variable that turns off checks while 35 | // trusted code is initializing and rely on that trusted code to not 36 | // allow untrusted inputs to reach require. This code should run 37 | // before any HTTP select loops are entered, so the major vectors 38 | // for untrusted inputs have not yet attached. 39 | let enabled = false; 40 | // We need to fail closed which means failing with a hook, and not 41 | // leaving enabled false after module exit. 42 | try { 43 | // eslint-disable-next-line no-use-before-define 44 | module.exports = beforeRequire; 45 | 46 | const String = ''.constructor; 47 | const { TypeError } = global; 48 | 49 | // eslint-disable-next-line global-require, id-length 50 | const fs = require('fs'); 51 | // eslint-disable-next-line global-require 52 | const path = require('path'); 53 | // eslint-disable-next-line global-require 54 | const { makeHook: makeResourceIntegrityHook } = require( 55 | './module-hooks/resource-integrity-hook.js'); 56 | // eslint-disable-next-line global-require 57 | const { makeHook: makeSensitiveModuleHook } = require( 58 | './module-hooks/sensitive-module-hook.js'); 59 | 60 | // Look for the project root by starting at the main module, which should 61 | // be loading these hooks via --cjs-loader and looking for a package.json 62 | // directory. 63 | let basedir = path.resolve(path.join(path.dirname(require.main.filename))); 64 | // eslint-disable-next-line no-sync 65 | while (!fs.existsSync(path.join(basedir, 'package.json'))) { 66 | basedir = path.dirname(basedir); 67 | } 68 | 69 | // Fetch configuration data 70 | const prodSources = (() => { 71 | try { 72 | // eslint-disable-next-line global-require 73 | return require(path.join(basedir, 'generated', 'prod-sources.json')); 74 | } catch (exc) { 75 | return null; 76 | } 77 | })(); 78 | // eslint-disable-next-line global-require 79 | const { sensitiveModules } = require(path.join(basedir, 'package.json')); 80 | 81 | // Create hooks 82 | const riHook = makeResourceIntegrityHook(prodSources, basedir, false); 83 | const smHook = makeSensitiveModuleHook(sensitiveModules, basedir, false); 84 | 85 | // eslint-disable-next-line no-inner-declarations 86 | function beforeRequire(importingFile, importingId, requiredId, resolve) { 87 | if (typeof resolve !== 'function') { 88 | throw new TypeError(); 89 | } 90 | importingFile = String(importingFile); 91 | importingId = String(importingId); 92 | requiredId = String(requiredId); 93 | 94 | if (enabled) { 95 | return smHook( 96 | importingFile, importingId, 97 | riHook(importingFile, importingId, requiredId, resolve), 98 | resolve); 99 | } 100 | return requiredId; 101 | } 102 | 103 | beforeRequire.flush = () => { 104 | if (typeof riHook.flush === 'function') { 105 | riHook.flush(); 106 | } 107 | if (typeof smHook.flush === 'function') { 108 | smHook.flush(); 109 | } 110 | }; 111 | } finally { 112 | // Exit privileged mode 113 | enabled = true; 114 | } 115 | 116 | module.exports.setInsecure = (insecure) => { 117 | if (insecure === true) { 118 | enabled = false; 119 | } 120 | }; 121 | -------------------------------------------------------------------------------- /scripts/markdown-table-of-contents.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * @license 5 | * Copyright 2018 Google LLC 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * https://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | /** 21 | * @fileoverview 22 | * Do just enough Markdown parsing to assemble a table of contents 23 | * and make sure there are link targets in both the Github hosted 24 | * version and that hosted at npmjs.com/package/.... 25 | */ 26 | 27 | 'use strict'; 28 | 29 | // eslint-disable-next-line id-length 30 | const fs = require('fs'); 31 | const process = require('process'); 32 | 33 | const { create } = Object; 34 | 35 | function tableOfContentsFor(filename, markdown) { 36 | const toc = []; 37 | let lastDepth = 0; 38 | const ids = create(null); 39 | 40 | const lines = markdown.split(/\n/g); 41 | for (let i = 0, nLines = lines.length; i < nLines; ++i) { 42 | let line = lines[i]; 43 | { 44 | const match = line.match(/^(#{2,})/); 45 | if (match) { 46 | // Maybe autogenerate id 47 | const headerText = line.substring(match[0].length); 48 | if (!/`; 57 | } 58 | } 59 | } 60 | { 61 | const match = /^(#{2,})(.*?)<\/a>\s*$/.exec(line); 62 | if (match) { 63 | const depth = match[1].length - 1; 64 | const [ , , text, id ] = match; 65 | if (id in ids) { 66 | throw new Error(`${ filename }:${ i }: Heading id ${ id } previously seen at line ${ ids[id] }`); 67 | } 68 | ids[id] = i; 69 | if (depth > lastDepth + 1) { 70 | throw new Error( 71 | `${ filename }:${ i }: Heading id ${ id } has depth ${ depth } which skips levels from ${ lastDepth }`); 72 | } 73 | const newText = text.replace(/^\s*|\s*$/g, ''); 74 | toc.push(`${ ' '.repeat(depth - 1) }* [${ newText }](#${ id })\n`); 75 | lastDepth = depth; 76 | } else if (/^##/.test(line)) { 77 | throw new Error(`${ filename }:${ i }: Heading lacks identifier`); 78 | } 79 | } 80 | lines[i] = line; 81 | } 82 | return { 83 | markdown: lines.join('\n'), 84 | toc: toc.join(''), 85 | }; 86 | } 87 | 88 | function replaceTableOfContentsIn(filename, markdown, toc) { 89 | const match = /(\n\n)(?:[^\n]|\n(?!\n)/.exec(markdown); 90 | if (match) { 91 | return `${ markdown.substring(0, match.index) }${ match[1] }${ toc }${ match[2] }${ 92 | markdown.substring(match.index + match[0].length) }`; 93 | } 94 | if (toc) { 95 | throw new Error(`${ filename }: Cannot find delimited space for the table of contents`); 96 | } 97 | return markdown; 98 | } 99 | 100 | if (require.main === module) { 101 | const [ , , ...filenames ] = process.argv; 102 | for (const filename of filenames) { 103 | // eslint-disable-next-line no-sync 104 | const originalContent = fs.readFileSync(filename, { encoding: 'utf8' }); 105 | // eslint-disable-next-line prefer-const 106 | let { markdown, toc } = tableOfContentsFor(filename, originalContent); 107 | 108 | markdown = replaceTableOfContentsIn(filename, markdown, toc); 109 | 110 | if (originalContent !== markdown) { 111 | // eslint-disable-next-line no-sync 112 | fs.writeFileSync(filename, markdown, { encoding: 'utf8' }); 113 | } 114 | } 115 | } 116 | 117 | module.exports = { 118 | tableOfContentsFor, 119 | replaceTableOfContentsIn, 120 | }; 121 | -------------------------------------------------------------------------------- /test/cases/end-to-end/login-case.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | 'use strict'; 19 | 20 | /* eslint array-element-newline: 0 */ 21 | 22 | const { URL } = require('url'); 23 | 24 | // Test the login flow 25 | // 1. Request the login page. 26 | // 2. Submit the form with an email. 27 | // 3. Redirect to a page and notice that the login link now has a username. 28 | 29 | module.exports = { 30 | requests: (baseUrl) => [ 31 | { 32 | req: { 33 | uri: new URL('/login?cont=/echo', baseUrl).href, 34 | method: 'GET', 35 | }, 36 | res: { 37 | body: [ 38 | '', 39 | '', 40 | '', 41 | 'Login', 42 | '', 44 | '', 45 | '', 46 | '', 47 | '

Login

', 48 | '
', 49 | '', 50 | '', 51 | '', 52 | ``, 53 | '
There is no password input since testing a credential store', 54 | 'is out of scope for this attack review, and requiring', 55 | 'credentials or using a federated service like oauth would', 56 | 'complicate running locally and testing as different users.
', 57 | '
', 58 | '', 59 | `
`, 60 | '', 61 | '', 62 | '', 63 | '', 64 | ], 65 | logs: { 66 | stderr: '', 67 | stdout: 'GET /login?cont=/echo\n', 68 | }, 69 | statusCode: 200, 70 | }, 71 | }, 72 | { 73 | req: (lastReponse, { csrf }) => ({ 74 | uri: new URL('/login', baseUrl).href, 75 | form: { 76 | email: 'foo@bar.com', 77 | cont: `${ baseUrl.origin }/echo`, 78 | _csrf: csrf, 79 | }, 80 | method: 'POST', 81 | }), 82 | res: { 83 | headers: { 84 | location: new URL('/echo', baseUrl).href, 85 | }, 86 | body: [ '' ], 87 | logs: { 88 | stderr: '', 89 | stdout: 'POST /login\n', 90 | }, 91 | statusCode: 302, 92 | }, 93 | }, 94 | { 95 | req: { 96 | uri: new URL('/echo', baseUrl).href, 97 | }, 98 | res: { 99 | body: [ 100 | '', 101 | '', 102 | '', 103 | 'Database Echo', 104 | '', 106 | '', 107 | '', 108 | '', 109 | '
', 110 | 'Anonymous', 111 | '', 112 | '
', 113 | '', 114 | '', 115 | '
', 116 | '
', 117 | '

Echo

', 118 | '', 119 | '', 120 | '', 121 | '', 122 | '', 123 | '', 124 | '', 125 | '
Hello
World
', 126 | '', 127 | '', 128 | ], 129 | logs: { 130 | stderr: '', 131 | stdout: 'GET /echo\necho sending SELECT \'World\' AS "Hello"\n', 132 | }, 133 | statusCode: 200, 134 | }, 135 | }, 136 | ], 137 | }; 138 | -------------------------------------------------------------------------------- /test/resource-integrity-hook-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | 'use strict'; 19 | 20 | const path = require('path'); 21 | const { expect } = require('chai'); 22 | const { describe, it } = require('mocha'); 23 | 24 | const { makeHook } = require('../lib/framework/module-hooks/resource-integrity-hook.js'); 25 | const { runHook } = require('./run-hook.js'); 26 | 27 | const basedir = path.resolve(path.join(__dirname, '..')); 28 | 29 | const resultOnReject = require.resolve('../lib/framework/module-hooks/innocuous.js'); 30 | 31 | describe('resource-integrity-hook', () => { 32 | const config = { 33 | // As reported by `shasum -a 256 test/file-with-known-hash.js` 34 | '9c7bbbbab6d0bdaf8ab15a94f42324488280e47e1531a218e687b95d30bf376d': [ 'test/file-with-known-hash.js' ], 35 | 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa': [ 'test/file-with-wrong-hash.js' ], 36 | }; 37 | describe('reportOnly', () => { 38 | const hook = makeHook(config, basedir, true); 39 | it('match', () => { 40 | const target = './file-with-known-hash.js'; 41 | // Test hash caching 42 | for (let retries = 2; --retries >= 0;) { 43 | expect(runHook(hook, 'source.js', target)) 44 | .to.deep.equal({ 45 | result: target, 46 | stderr: '', 47 | stdout: '', 48 | }); 49 | } 50 | }); 51 | it('mismatch', () => { 52 | const target = './file-with-wrong-hash.js'; 53 | // Test hash caching 54 | for (let retries = 2; --retries >= 0;) { 55 | expect(runHook(hook, 'source.js', target)) 56 | .to.deep.equal({ 57 | result: target, 58 | stderr: ( 59 | 'lib/framework/module-hooks/resource-integrity-hook.js: ' + 60 | 'Blocking require("./file-with-wrong-hash.js") ' + 61 | 'by test/source.js\n'), 62 | stdout: '', 63 | }); 64 | } 65 | }); 66 | it('404', () => { 67 | const target = './no-such-file.js'; 68 | expect(runHook(hook, 'source.js', target)) 69 | .to.deep.equal({ 70 | result: target, 71 | stderr: ( 72 | 'lib/framework/module-hooks/resource-integrity-hook.js: ' + 73 | 'Blocking require("./no-such-file.js") ' + 74 | 'by test/source.js\n'), 75 | stdout: '', 76 | }); 77 | }); 78 | }); 79 | describe('active', () => { 80 | const hook = makeHook(config, basedir, false); 81 | it('match', () => { 82 | const target = './file-with-known-hash.js'; 83 | // Test hash caching 84 | for (let retries = 2; --retries >= 0;) { 85 | expect(runHook(hook, 'source.js', target)) 86 | .to.deep.equal({ 87 | result: target, 88 | stderr: '', 89 | stdout: '', 90 | }); 91 | } 92 | }); 93 | it('mismatch', () => { 94 | const target = './file-with-wrong-hash.js'; 95 | expect(runHook(hook, 'source.js', target)) 96 | .to.deep.equal({ 97 | result: resultOnReject, 98 | stderr: ( 99 | 'lib/framework/module-hooks/resource-integrity-hook.js: ' + 100 | 'Blocking require("./file-with-wrong-hash.js") ' + 101 | 'by test/source.js\n'), 102 | stdout: '', 103 | }); 104 | }); 105 | it('404', () => { 106 | const target = './no-such-file.js'; 107 | expect(runHook(hook, 'source.js', target)) 108 | .to.deep.equal({ 109 | result: resultOnReject, 110 | stderr: ( 111 | 'lib/framework/module-hooks/resource-integrity-hook.js: ' + 112 | 'Blocking require("./no-such-file.js") ' + 113 | 'by test/source.js\n'), 114 | stdout: '', 115 | }); 116 | }); 117 | }); 118 | it('fail-closed', () => { 119 | const hook = makeHook(null, basedir, false); 120 | const target = './file-with-known-hash.js'; 121 | expect(runHook(hook, 'source.js', target)) 122 | .to.deep.equal({ 123 | result: resultOnReject, 124 | stderr: ( 125 | 'lib/framework/module-hooks/resource-integrity-hook.js: ' + 126 | 'Blocking require("./file-with-known-hash.js") by test/source.js\n' 127 | ), 128 | stdout: '', 129 | }); 130 | }); 131 | }); 132 | -------------------------------------------------------------------------------- /test/db-tables-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | 'use strict'; 19 | 20 | const { expect } = require('chai'); 21 | const { describe, it } = require('mocha'); 22 | const safesql = require('safesql'); 23 | 24 | const { createTables, clearTables, initializeTablesWithTestData } = 25 | require('../lib/db-tables.js'); 26 | 27 | module.exports = (makePool) => { 28 | function dbIt(name, withPool, withClient) { 29 | it(name, function dbTest(done) { 30 | // Setting up the database takes some time. 31 | // Times are in millis. 32 | // eslint-disable-next-line no-magic-numbers, no-invalid-this 33 | this.timeout(5000); 34 | // eslint-disable-next-line no-magic-numbers, no-invalid-this 35 | this.slow(500); 36 | 37 | makePool().then( 38 | (pool) => { 39 | let client = null; 40 | function finish(exc) { 41 | if (client) { 42 | client.release(); 43 | client = null; 44 | } 45 | pool.end(); 46 | if (exc) { 47 | return done(exc); 48 | } 49 | return done(); 50 | } 51 | function finishErr(exc) { 52 | finish(exc || new Error()); 53 | } 54 | 55 | function usePool(exc) { 56 | if (exc) { 57 | finish(exc); 58 | return; 59 | } 60 | pool.connect().then( 61 | (aClient) => { 62 | client = aClient; 63 | withClient(client, finish, finishErr); 64 | }, 65 | finishErr); 66 | } 67 | 68 | setTimeout( 69 | () => withPool(pool, usePool, finishErr), 70 | // eslint-disable-next-line no-magic-numbers 71 | 50); 72 | }, 73 | (dbErr) => { 74 | done(dbErr || new Error()); 75 | }); 76 | }); 77 | } 78 | 79 | describe('db-tables', () => { 80 | dbIt( 81 | 'createTables', 82 | (pool, finish, finishErr) => createTables(pool).then(() => finish(), finishErr), 83 | (client, finish, finishErr) => 84 | client.query(safesql.pg`SELECT * FROM Accounts`).then( 85 | ({ rowCount, fields }) => { 86 | try { 87 | expect({ rowCount, fields: fields.map(({ name }) => name) }) 88 | .to.deep.equals({ 89 | rowCount: 0, 90 | fields: [ 'aid', 'displayname', 'displaynamehtml', 'publicurl', 'created' ], 91 | }); 92 | } catch (exc) { 93 | finish(exc); 94 | return; 95 | } 96 | finish(); 97 | }, 98 | finishErr)); 99 | 100 | dbIt( 101 | 'initializeTablesWithTestData', 102 | (pool, finish, finishErr) => initializeTablesWithTestData(pool).then(() => finish(), finishErr), 103 | (client, finish, finishErr) => 104 | client.query(safesql.pg`SELECT * FROM Accounts`).then( 105 | ({ rowCount, fields }) => { 106 | try { 107 | expect({ rowCount, fields: fields.map(({ name }) => name) }) 108 | .to.deep.equals({ 109 | rowCount: 4, 110 | fields: [ 'aid', 'displayname', 'displaynamehtml', 'publicurl', 'created' ], 111 | }); 112 | } catch (exc) { 113 | finish(exc); 114 | return; 115 | } 116 | finish(); 117 | }, 118 | finishErr)); 119 | 120 | dbIt( 121 | 'clearTables', 122 | (pool, finish, finishErr) => clearTables(pool).then(() => finish(), finishErr), 123 | (client, finish, finishErr) => 124 | client.query(safesql.pg`SELECT * FROM Accounts`).then( 125 | ({ rowCount, fields }) => { 126 | try { 127 | expect({ rowCount, fields: fields.map(({ name }) => name) }) 128 | .to.deep.equals({ 129 | rowCount: 0, 130 | fields: [ 'aid', 'displayname', 'displaynamehtml', 'publicurl', 'created' ], 131 | }); 132 | } catch (exc) { 133 | finish(exc); 134 | return; 135 | } 136 | finish(); 137 | }, 138 | finishErr)); 139 | }); 140 | }; 141 | -------------------------------------------------------------------------------- /test/doc-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | 'use strict'; 19 | 20 | /** 21 | * @fileoverview 22 | * Some sanity checks for README.md files. 23 | * 24 | * - Checks example code in README.md files for well-formedness 25 | * - Runs example code that has a comment like //! expected output 26 | * - Looks for undefined link targets like [foo][never defines]. 27 | * - Checks that tables of contents are up-to-date with the pages header structure. 28 | */ 29 | 30 | /* eslint no-sync: 0 */ 31 | 32 | const { expect } = require('chai'); 33 | const { describe, it } = require('mocha'); 34 | 35 | // eslint-disable-next-line id-length 36 | const fs = require('fs'); 37 | // eslint-disable-next-line id-length 38 | const vm = require('vm'); 39 | 40 | const { 41 | tableOfContentsFor, 42 | replaceTableOfContentsIn, 43 | } = require('../scripts/markdown-table-of-contents.js'); 44 | 45 | const markdownPaths = { 46 | 'README.md': require.resolve('../README.md'), 47 | }; 48 | 49 | function lookForUndefinedLinks(markdownPath) { 50 | let markdown = fs.readFileSync(markdownPath, { encoding: 'utf8' }); 51 | 52 | // Strip out code blocks. 53 | markdown = markdown.replace( 54 | /^(\s*`{3,})\w*\n(?:[^`]|`(?!``))*\n\1\n/mg, '$1CODE\n'); 55 | markdown = markdown.replace( 56 | /`[^`\r\n]+`/g, ' CODE '); 57 | 58 | // Extract link names. 59 | const namedLinks = new Set(); 60 | for (;;) { 61 | const original = markdown; 62 | markdown = markdown.replace(/^\[(.*?)\]:[ \t]*\S.*/m, (all, name) => { 63 | namedLinks.add(name); 64 | return ``; 65 | }); 66 | if (original === markdown) { 67 | break; 68 | } 69 | } 70 | 71 | let undefinedLinks = new Set(); 72 | function extractLink(whole, text, target) { 73 | target = target || text; 74 | if (!namedLinks.has(target)) { 75 | undefinedLinks.add(target); 76 | } 77 | return ' LINK '; 78 | } 79 | // Look at links. 80 | for (;;) { 81 | const original = markdown; 82 | markdown = markdown.replace( 83 | /(?:^|[^\]\\])\[((?:[^\\\]]|\\.)+)\]\[(.*?)\]/, 84 | extractLink); 85 | if (original === markdown) { 86 | break; 87 | } 88 | } 89 | 90 | undefinedLinks = Array.from(undefinedLinks); 91 | expect(undefinedLinks).to.deep.equals([]); 92 | } 93 | 94 | 95 | function hackyUpdoc(markdownPath) { 96 | const markdown = fs.readFileSync(markdownPath, { encoding: 'utf8' }); 97 | 98 | // Strip out code blocks. 99 | const fencedJsBlockPattern = /^(\s*)```js(\n(?:[^`]|`(?!``))*)\n\1```\n/mg; 100 | for (let match; (match = fencedJsBlockPattern.exec(markdown));) { 101 | const [ , , code ] = match; 102 | const lineOffset = markdown.substring(0, match.index).split(/\n/g).length; 103 | 104 | it(`Line ${ lineOffset }`, () => { 105 | let tweakedCode = code.replace( 106 | /^console[.]log\((.*)\);\n\/\/! ?(.*)/mg, 107 | (whole, expr, want) => `expect(String(${ expr })).to.equal(${ JSON.stringify(want) });\n`); 108 | 109 | if (tweakedCode === code) { 110 | // Not a test. Just check well-formedness 111 | tweakedCode = `(function () {\n${ tweakedCode }\n})`; 112 | } 113 | 114 | const script = new vm.Script(tweakedCode, { filename: markdownPath, lineOffset }); 115 | 116 | script.runInNewContext({ require, expect }); 117 | }); 118 | } 119 | } 120 | 121 | 122 | describe('doc', () => { 123 | describe('links', () => { 124 | for (const [ name, mdPath ] of Object.entries(markdownPaths)) { 125 | it(name, () => { 126 | lookForUndefinedLinks(mdPath); 127 | }); 128 | } 129 | }); 130 | describe('code examples', () => { 131 | for (const [ name, mdPath ] of Object.entries(markdownPaths)) { 132 | describe(name, () => { 133 | hackyUpdoc(mdPath); 134 | }); 135 | } 136 | }); 137 | describe('tocs', () => { 138 | for (const [ name, mdPath ] of Object.entries(markdownPaths)) { 139 | it(name, () => { 140 | const originalMarkdown = fs.readFileSync(mdPath, { encoding: 'utf8' }); 141 | const { markdown, toc } = tableOfContentsFor(mdPath, originalMarkdown); 142 | const markdownProcessed = replaceTableOfContentsIn(mdPath, markdown, toc); 143 | expect(originalMarkdown).to.equal(markdownProcessed, mdPath); 144 | }); 145 | } 146 | }); 147 | }); 148 | -------------------------------------------------------------------------------- /test/cases/end-to-end/login-logout-case.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | 'use strict'; 19 | 20 | /* eslint array-element-newline: 0 */ 21 | 22 | const { URL } = require('url'); 23 | 24 | module.exports = { 25 | requests: (baseUrl) => [ 26 | { 27 | req: { 28 | uri: new URL('/login', baseUrl).href, 29 | }, 30 | res: { 31 | body: 'IGNORE', 32 | logs: { 33 | stderr: '', 34 | stdout: 'GET /login\n', 35 | }, 36 | statusCode: 200, 37 | }, 38 | }, 39 | { 40 | req: (lastReponse, { csrf }) => ({ 41 | uri: new URL('/login', baseUrl).href, 42 | method: 'POST', 43 | form: { 44 | _csrf: csrf, 45 | email: 'ada@example.com', 46 | cont: `${ baseUrl.origin }/echo`, 47 | }, 48 | }), 49 | res: { 50 | body: [ '' ], 51 | headers: { 52 | location: `${ baseUrl.origin }/echo`, 53 | }, 54 | logs: { 55 | stderr: '', 56 | stdout: 'POST /login\n', 57 | }, 58 | statusCode: 302, 59 | }, 60 | }, 61 | { 62 | req: { 63 | uri: new URL('/echo', baseUrl).href, 64 | }, 65 | res: { 66 | body: [ 67 | '', 68 | '', 69 | '', 70 | 'Database Echo', 71 | '', 73 | '', 74 | '', 75 | '', 76 | '
', 77 | // Got user name. See db-tables.js 78 | 'Ada', 79 | '', 80 | '
', 81 | '', 82 | '', 83 | '
', 84 | '
', 85 | '

Echo

', 86 | '', 87 | '', 88 | '', 89 | '', 90 | '', 91 | '', 92 | '', 93 | '
Hello
World
', 94 | '', 95 | '', 96 | ], 97 | logs: { 98 | stderr: '', 99 | stdout: 'GET /echo\necho sending SELECT \'World\' AS "Hello"\n', 100 | }, 101 | statusCode: 200, 102 | }, 103 | }, 104 | { 105 | req: (lastReponse, { csrf }) => ({ 106 | uri: new URL('/logout', baseUrl).href, 107 | headers: { 108 | Referer: `${ baseUrl.origin }/echo`, 109 | }, 110 | method: 'POST', 111 | form: { 112 | _csrf: csrf, 113 | }, 114 | }), 115 | res: { 116 | body: [ '' ], 117 | headers: { 118 | location: `${ baseUrl.origin }/echo`, 119 | }, 120 | logs: { 121 | stderr: '', 122 | stdout: 'POST /logout\n', 123 | }, 124 | statusCode: 302, 125 | }, 126 | }, 127 | { 128 | req: { 129 | uri: new URL('/echo', baseUrl).href, 130 | }, 131 | res: { 132 | body: [ 133 | '', 134 | '', 135 | '', 136 | 'Database Echo', 137 | '', 139 | '', 140 | '', 141 | '', 142 | '
', 143 | // No user name. 144 | 'login', 145 | '
', 146 | '

Echo

', 147 | '', 148 | '', 149 | '', 150 | '', 151 | '', 152 | '', 153 | '', 154 | '
Hello
World
', 155 | '', 156 | '', 157 | ], 158 | logs: { 159 | stderr: '', 160 | stdout: 'GET /echo\necho sending SELECT \'World\' AS "Hello"\n', 161 | }, 162 | statusCode: 200, 163 | }, 164 | }, 165 | ], 166 | }; 167 | -------------------------------------------------------------------------------- /lib/framework/module-hooks/resource-integrity-hook.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | /** 19 | * @fileoverview 20 | * A factory for hooks that prevent require of files not on a production whitelist 21 | * such as that generated by scripts/generate-production-source-list.js 22 | */ 23 | 24 | 'use strict'; 25 | 26 | // SENSITIVE - Trusted to preserve guarantee that the only code that 27 | // loads in production was written or `npm install`ed by a trusted 28 | // developer for use in production. 29 | 30 | // GUARANTEE - the output of makeHook only returns a module M if that 31 | // the hash of the content at require.resolve(M) appears on the production 32 | // whitelist. 33 | // This guarantee should survive in the face of an attacker who can 34 | // create files including symlinks and hardlinks but makes no 35 | // guarantee in the face of race conditions related to renaming 36 | // ancestor directories, replacing file content, or unmounting file 37 | // systems. 38 | 39 | // TODO: do we need to check the hash of ./innocuous.js's before 40 | // returning it? That seems a vector that doesn't require winning 41 | // a race condition. 42 | // 43 | // TODO: document caveats related to overwrites. 44 | // 45 | // If we cache hashes, we are assuming no overwrites and cannot trust 46 | // mtime more than we trust content. 47 | // 48 | // But commonly used modules assume the path to the require cache is fast, 49 | // so we might have to be optimistic and just tell people to run node as 50 | // a uid that cannot write source files if they want resource integrity 51 | // to be strong. 52 | // 53 | // TODO: Figure out how to phrase this guarantee optimistically assuming 54 | // some care taken with file permissions. 55 | // 56 | // TODO: Maybe gather stats on how many modules are required more than 57 | // once and lazily: lib/handlers? 58 | // 59 | // TODO: Do we reduce the attack surface by having startup scripts 60 | // `chmod -R ugo-w lib node_modules` even if the server process owns 61 | // its source files? 62 | 63 | // eslint-disable-next-line no-use-before-define 64 | exports.makeHook = makeHook; 65 | 66 | const { 67 | createHash, 68 | Hash: { 69 | prototype: { 70 | digest: digestHash, 71 | update: updateHash, 72 | }, 73 | }, 74 | } = require('crypto'); 75 | const { readFileSync } = require('fs'); 76 | const { join, relative } = require('path'); 77 | 78 | const { isBuiltinModuleId } = require('../builtin-module-ids.js'); 79 | 80 | const { create, hasOwnProperty } = Object; 81 | const { apply } = Reflect; 82 | // eslint-disable-next-line id-blacklist 83 | const { error: consoleerror, warn: consolewarn } = console; 84 | 85 | function makeHook(hashesToSourceLists, basedir, reportOnly) { 86 | const moduleid = relative(basedir, module.filename); 87 | if (!(hashesToSourceLists && typeof hashesToSourceLists === 'object')) { 88 | hashesToSourceLists = create(null); 89 | } 90 | 91 | // Don't hash modules more than once. 92 | // Assumes that modules don't change on disk. 93 | const hashCache = new Map(); 94 | 95 | function hashFor(file) { 96 | if (hashCache.has(file)) { 97 | return hashCache.get(file); 98 | } 99 | 100 | let key = null; 101 | let content = null; 102 | try { 103 | content = readFileSync(file); 104 | } catch (exc) { 105 | consoleerror(`${ moduleid }: ${ exc.message }`); 106 | } 107 | if (content !== null) { 108 | const hash = createHash('sha256'); 109 | key = apply( 110 | digestHash, 111 | apply(updateHash, hash, [ content ]), 112 | [ 'hex' ]); 113 | } 114 | 115 | hashCache.set(file, key); 116 | return key; 117 | } 118 | 119 | return function resourceIntegrityHook( 120 | importingFile, importingId, requiredId, resolveFilename) { 121 | if (isBuiltinModuleId(requiredId)) { 122 | return requiredId; 123 | } 124 | let target = null; 125 | try { 126 | target = resolveFilename(requiredId); 127 | } catch (exc) { 128 | // We fall-through to hash mismatch below. 129 | } 130 | if (target !== null) { 131 | const key = hashFor(target); 132 | if (key && apply(hasOwnProperty, hashesToSourceLists, [ key ])) { 133 | return requiredId; 134 | } 135 | } 136 | 137 | consolewarn( 138 | `${ moduleid }: Blocking require(${ JSON.stringify(requiredId) }) by ${ relative(basedir, importingFile) }`); 139 | if (reportOnly) { 140 | return requiredId; 141 | } 142 | return join(__dirname, 'innocuous.js'); 143 | }; 144 | } 145 | -------------------------------------------------------------------------------- /test/cases/end-to-end/drive-by-post-case.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | 'use strict'; 19 | 20 | /* eslint array-element-newline: 0 */ 21 | 22 | const { URL } = require('url'); 23 | 24 | const now = Number(new Date('2019-04-22 12:00:00Z')); 25 | 26 | // Logs in 27 | // Drafts a post 28 | // Uploads some images 29 | // Previews the post. 30 | // Commits 31 | // Views the index page with the new post. 32 | const indexRelUrl = `/?now=${ now }&offset=4`; 33 | 34 | module.exports = { 35 | requests: (baseUrl, { isProduction }) => [ 36 | // User logs in 37 | { 38 | req: { 39 | uri: new URL('/login', baseUrl).href, 40 | }, 41 | res: { 42 | body: 'IGNORE', 43 | logs: { 44 | stderr: '', 45 | stdout: 'GET /login\n', 46 | }, 47 | statusCode: 200, 48 | }, 49 | }, 50 | { 51 | req: (lastResponse, { csrf }) => ({ 52 | uri: new URL('/login', baseUrl).href, 53 | method: 'POST', 54 | form: { 55 | cont: '/', 56 | email: 'ada@example.com', 57 | _csrf: csrf, 58 | }, 59 | }), 60 | res: { 61 | body: [ '' ], 62 | logs: { 63 | stderr: '', 64 | stdout: 'POST /login\n', 65 | }, 66 | headers: { 67 | location: new URL('/', baseUrl).href, 68 | }, 69 | statusCode: 302, 70 | }, 71 | }, 72 | // Later user visits a page that posts cross-origin. 73 | { 74 | req: { 75 | uri: new URL('/post', baseUrl).href, 76 | method: 'POST', 77 | formData: { 78 | body: 'Eat at Joe\'s!', 79 | 'public': 'true', 80 | now, 81 | }, 82 | }, 83 | res: { 84 | body: [ 85 | '', 86 | '', 87 | '', 88 | 'Error', 89 | '', 91 | '', 92 | '', 93 | '', 94 | '
', 95 | 'login', 96 | (isProduction ? 97 | '
Something went wrong' : 98 | '
Error: CSRF Token Not Found'), 99 | '', 100 | ], 101 | logs: { 102 | stderr: 'Error: CSRF Token Not Found\n', 103 | stdout: 'POST /post\n', 104 | }, 105 | headers: { 106 | location: void 0, 107 | }, 108 | statusCode: 500, 109 | }, 110 | }, 111 | { 112 | req: { 113 | uri: new URL(indexRelUrl, baseUrl).href, 114 | }, 115 | res: { 116 | body: [ 117 | '', 118 | '', 119 | '', 120 | 'Attack Review Testbed', 121 | '', 123 | '', 124 | '', 125 | '', 126 | '
', 127 | 'Ada', 128 | '', 129 | `
', 131 | '', 132 | '', 133 | '
', 134 | '
', 135 | '

Recent Posts

', 136 | '
    ', 137 | '
  1. ', 138 | // Since offset is set, 1 post from canned data 139 | '', 140 | '', 141 | 'Fae', 142 | '', 143 | '', 144 | 'a week ago', 145 | '
    (It is probably insecure)
    ', 146 | '
  2. ', 147 | // No "Eat at Joe's" post. 148 | '
', 149 | '
', 150 | '', 151 | '', 152 | '', 153 | '
', 154 | '', 155 | '', 156 | ], 157 | logs: { 158 | stdout: `GET ${ indexRelUrl }\n`, 159 | }, 160 | }, 161 | }, 162 | 163 | ], 164 | }; 165 | -------------------------------------------------------------------------------- /test/end-to-end-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | 'use strict'; 19 | 20 | /* eslint id-blacklist: 0, no-multi-str: 0, no-magic-numbers: 0, array-element-newline: 0 */ 21 | 22 | const { describe } = require('mocha'); 23 | 24 | // Make sure that we stub out modules that interact badly with 25 | // some of our protective mechanisms 26 | require('./main-test.js'); 27 | 28 | // eslint-disable-next-line id-length 29 | const path = require('path'); 30 | const process = require('process'); 31 | const { URL } = require('url'); 32 | const { startCapture, startIntercept, stopCapture, stopIntercept } = require('capture-console'); 33 | const { clearTables, initializeTablesWithTestData } = require('../lib/db-tables.js'); 34 | 35 | const { start } = require('../lib/server.js'); 36 | const runEndToEndCases = require('./end-to-end-common.js'); 37 | 38 | const hostName = '127.0.0.1'; 39 | const rootDir = path.resolve(path.join(__dirname, '..')); 40 | 41 | function noop() { 42 | // This function body left intentionally blank. 43 | } 44 | 45 | module.exports = (makePool) => { 46 | function withServer(onStart, { cannedData = true }) { 47 | let i = 0; 48 | 49 | // Substitute for our nonce generator, a function that produces predictable 50 | // output, so we don't have to deal with nonces in output. 51 | function notReallyUnguessable() { 52 | const str = `${ i++ }`; 53 | return 'x'.repeat(32 - str.length) + str; 54 | } 55 | 56 | const quiet = true; 57 | const startTrapLog = quiet ? startIntercept : startCapture; 58 | const stopTrapLog = quiet ? stopIntercept : stopCapture; 59 | 60 | function withPool() { 61 | return new Promise((resolve, reject) => { 62 | makePool().then( 63 | (pool) => { 64 | const setup = cannedData ? initializeTablesWithTestData : clearTables; 65 | setup(pool).then( 66 | () => { 67 | resolve(pool); 68 | }, 69 | (exc) => { 70 | pool.end(); 71 | reject(exc); 72 | }); 73 | }, 74 | reject); 75 | }); 76 | } 77 | 78 | 79 | withPool().then( 80 | (database) => { 81 | let stdout = ''; 82 | let stderr = ''; 83 | startTrapLog(process.stdout, (chunk) => { 84 | stdout += chunk; 85 | }); 86 | startTrapLog(process.stderr, (chunk) => { 87 | stderr += chunk; 88 | }); 89 | const { stop } = start( 90 | { hostName, port: 0, rootDir, database }, 91 | (err, actualPort) => { 92 | const url = new URL(`http://${ hostName }:${ actualPort }`); 93 | onStart( 94 | err, url, 95 | () => { 96 | try { 97 | stop(); 98 | database.end(); 99 | } finally { 100 | stopTrapLog(process.stderr); 101 | stopTrapLog(process.stdout); 102 | } 103 | }, 104 | () => { 105 | const errText = stderr; 106 | const outText = stdout; 107 | stderr = ''; 108 | stdout = ''; 109 | return { stderr: errText, stdout: outText }; 110 | }); 111 | }, 112 | notReallyUnguessable); 113 | }, 114 | (dbConfigErr) => { 115 | onStart(dbConfigErr, new URL('about:invalid'), noop, () => ({})); 116 | }); 117 | } 118 | 119 | function serverTestFunction(testFun) { 120 | return function serverTest(done) { 121 | this.slow(250); // eslint-disable-line no-invalid-this 122 | withServer( 123 | (startErr, url, stop, logs) => { 124 | let closed = false; 125 | function closeAndEnd(closeErr) { 126 | if (!closed) { 127 | closed = true; 128 | stop(closeErr); 129 | if (closeErr) { 130 | return done(closeErr); 131 | } 132 | return done(); 133 | } 134 | return null; 135 | } 136 | if (startErr) { 137 | closeAndEnd(startErr); 138 | return; 139 | } 140 | try { 141 | testFun(url, closeAndEnd, logs); 142 | } catch (exc) { 143 | closeAndEnd(exc); 144 | throw exc; 145 | } 146 | }, 147 | {}); 148 | }; 149 | } 150 | 151 | describe('end-to-end', () => { 152 | runEndToEndCases(serverTestFunction, { isProduction: false, root: rootDir }); 153 | }); 154 | }; 155 | -------------------------------------------------------------------------------- /lib/poorly-written-linkifier.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | 'use strict'; 19 | 20 | /** 21 | * @fileoverview 22 | * A linkifier that adds link tags to link-like substrings in HTML 23 | * text nodes. 24 | */ 25 | 26 | const tlds = require('tlds'); 27 | 28 | function descLength(stra, strb) { 29 | return strb.length - stra.length; 30 | } 31 | 32 | const replacements = { 33 | '<': '<', 34 | '>': '>', 35 | '"': '"', 36 | }; 37 | 38 | function normhtml(str) { 39 | // Like escapehtml but doesn't escape & since we don't want to have 40 | // to decode and then reencode. 41 | return str.replace(/[<>"]/g, (chr) => replacements[chr]); 42 | } 43 | 44 | // A registry name should end in a TLD. We sort TLDs so that the RegExp greedily matches 45 | // the longest TLD: e.g. .community should come before .com. 46 | const REG_NAME = String.raw`(?:[a-z0-9\-_~!$&'()*+,;=%]+[.])+(?:${ tlds.sort(descLength).join('|') })[.]?`; 47 | 48 | // TODO: range restrict 49 | // eslint-disable-next-line id-match 50 | const IPV4 = String.raw`\d+(?:\.\d+){3}(?=[/?#]|:\d)`; 51 | 52 | // TODO: count hex digits properly. 53 | // eslint-disable-next-line id-match 54 | const IPV6 = String.raw`\[(?!\])(?:[0-9a-f]+(?:[:][0-9a-f]+)*)?(?:[:][:](?:[0-9a-f]+(?:[:][0-9a-f]+)*))?\]`; 55 | 56 | const PROTOCOL_REGEX = String.raw`\b([a-z][a-z0-9+\-.]*):`; 57 | 58 | const PROTOCOL_PATTERN = new RegExp(`^${ PROTOCOL_REGEX }`, 'i'); 59 | 60 | const AUTHORITY_REGEX = String.raw`[^/:?#\s<>!,;]*`; 61 | const AUTHORITY_PATTERN = new RegExp(`^(?:[^:/#?]*:)?//(${ AUTHORITY_REGEX })`); 62 | 63 | /** If there's no scheme and the URL is not protocol relative, make it complete. */ 64 | function coerceToUrl(url) { 65 | const pmatch = PROTOCOL_PATTERN.exec(url); 66 | let scheme = null; 67 | if (pmatch) { 68 | scheme = pmatch[1].toLowerCase(); 69 | } else { 70 | scheme = 'http'; 71 | url = `//${ url }`; 72 | } 73 | const amatch = AUTHORITY_PATTERN.exec(url); 74 | const authority = amatch && amatch[1]; 75 | const schemeSpecificPart = amatch ? url.substring(amatch[0].length) : null; 76 | return { url, scheme, authority, schemeSpecificPart }; 77 | } 78 | 79 | const LINK_PATTERN = new RegExp( 80 | // We either need a scheme or something that is obviously an origin. 81 | String.raw`(?:` + 82 | // An optional protocol 83 | String.raw`(?:(?:${ PROTOCOL_REGEX })?\/\/)` + AUTHORITY_REGEX + 84 | // Host may be numeric, IPv6, or a registry name. 85 | String.raw`|` + 86 | String.raw`(?:${ REG_NAME }|${ IPV4 }|${ IPV6 })` + 87 | // There might be a port. 88 | String.raw`(?:[:]\d+)?` + 89 | String.raw`)` + 90 | // Match path, query and fragment parts until a space or a punctuation 91 | // character that is followed by a space. 92 | String.raw`(?:[^\s.!?,;<>]|[.!?,;](?![\s]|$))*`, 93 | 'ig'); 94 | 95 | const SCHEME_DATA = { 96 | // Show an email icon to the right. 97 | mailto: { attrs: ' class="ext-link email"' }, 98 | // Show a download icon to the right. 99 | ftp: { attrs: ' class="ext-link download"' }, 100 | http: { 101 | // An internal link. Do nothing. 102 | 'example.com'() { 103 | return { attrs: '' }; 104 | }, 105 | '*'(scheme, authority) { 106 | // Tell users which site they're going to. 107 | return { 108 | attrs: ' class="ext-link"', 109 | // Make origin apparent to users. 110 | text: ` (${ normhtml(authority) })`, 111 | }; 112 | }, 113 | }, 114 | https(url) { 115 | return SCHEME_DATA.http(url); 116 | }, 117 | easteregg: { attrs: ' title="🥚"' }, 118 | }; 119 | 120 | function linkify(html) { 121 | html = `${ html }`; 122 | 123 | // Pull tags out into a side-table. 124 | const sideTable = []; 125 | html = html.replace( 126 | /<\/?[A-Za-z](?:[^>"']|"[^"]*"|'[^']*')*>/g, 127 | (tag) => { 128 | const index = sideTable.length; 129 | sideTable.push(tag); 130 | return ``; 131 | }); 132 | 133 | function rewriteLink(href) { 134 | const { url, scheme, authority, schemeSpecificPart } = coerceToUrl(href); 135 | 136 | // Extract extra info from the URL. 137 | let extras = SCHEME_DATA[scheme]; 138 | if (extras && authority) { 139 | if (extras[authority]) { 140 | extras = extras[authority]; 141 | } else if (extras['*']) { 142 | extras = extras['*']; 143 | } 144 | } 145 | while (typeof extras === 'function') { 146 | extras = extras(scheme, authority, schemeSpecificPart); 147 | } 148 | const { attrs = '' } = extras || {}; 149 | 150 | return `${ href }`; 151 | } 152 | 153 | // Rewrite links 154 | html = html.replace(LINK_PATTERN, rewriteLink); 155 | 156 | // Reconsistute tags. 157 | html = html.replace( 158 | //g, 159 | (whole, index) => sideTable[index]); 160 | 161 | return html; 162 | } 163 | 164 | module.exports = linkify; 165 | -------------------------------------------------------------------------------- /lib/framework/delicate-globals-rewrite.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | 'use strict'; 19 | 20 | /** 21 | * @fileoverview 22 | * Lots of builtin code requires the global process variable. 23 | * Virtualizing it would slow that code down. 24 | * Instrumenting every non-builtin source file is actually the most 25 | * efficient way to deny user code to silently access process. 26 | * 27 | * Some code needs to legitimately use `new Function`, but the node 28 | * runtime does not use a V8 version with a sufficiently flexible 29 | * allow_code_generation_from_string callback API like V8 3.7 does. 30 | * We can proxy Function to instead use the vm module to load code 31 | * when there are explicit calls, and then use module whitelisting 32 | * to gate access to the vm module. 33 | * 34 | * This module provides a (code,filename)->code transform that does both. 35 | */ 36 | 37 | // SENSITIVE - receives privileged require function. 38 | 39 | // See ./lockdown.js for guarantees re process. 40 | 41 | // The guarantees re Function are mostly checked by the flag choice in 42 | // main, but the guarantee below states captures the negative space 43 | // around what the Function masking below aims to allow. 44 | 45 | // GUARANTEE - A module that doesn't explicitly invoke Function(...) or 46 | // new Function(...) shouldn't be able to implicitly load code as in 47 | // // Attacker controlled strings 48 | // let a = 'constructor', b = 'console.log(1337)'; 49 | // // Naive code 50 | // ({})[a][a](b)(); 51 | // so we don't need to audit all modules for possible implicit access to 52 | // the Function constructor. 53 | 54 | // eslint-disable-next-line no-use-before-define 55 | module.exports = rewriteCode; 56 | 57 | const { fromCharCode } = String; 58 | const { create, defineProperty } = Object; 59 | const { randomBytes } = require('crypto'); 60 | const codeLoadingFunctionProxy = require('./code-loading-function-proxy.js'); 61 | const wellFormednessCheck = require('./well-formedness-check.js'); 62 | const unprivilegedRequire = require('./unprivileged-require.js'); 63 | 64 | function rewriteCode(code, filename, fallbackRequire = unprivilegedRequire) { 65 | code = `${ code }`; 66 | const decoded = code.replace( 67 | /\\u([0-9A-Fa-f]{4})/g, 68 | // eslint-disable-next-line no-magic-numbers 69 | (whole, hex) => fromCharCode(parseInt(hex, 16))); 70 | 71 | const masks = []; 72 | 73 | // On first reference to Function, load one that uses this module's require to load code via vm. 74 | // This lets us deny implicit calls to the Function constructor. 75 | // Mask Function if it seems to be used as a constructor intentionally. 76 | if (/\bFunction\s*\(/.test(decoded)) { 77 | masks[masks.length] = (localRequire, mask) => { 78 | let fun = null; 79 | defineProperty( 80 | mask, 81 | 'Function', 82 | { 83 | get() { 84 | const req = localRequire || fallbackRequire; 85 | return fun || (fun = codeLoadingFunctionProxy(req, filename)); 86 | }, 87 | }); 88 | }; 89 | } 90 | 91 | // Mask process 92 | if (/process(?!\w)/.test(decoded)) { 93 | // TODO: get a process mask value that encapsulates module identity by passing require 94 | // to a module that returns a masking value. 95 | 96 | // Add a single use non-enumerable global so that two modules can't 97 | // conspire by having an early loading one compute a hash for a late 98 | // loading one. 99 | // eslint-disable-next-line no-magic-numbers 100 | masks[masks.length] = (localRequire, mask) => { 101 | let proc = null; 102 | defineProperty( 103 | mask, 104 | 'process', 105 | { 106 | get() { 107 | const req = localRequire || fallbackRequire; 108 | return proc || (proc = req('process')); 109 | }, 110 | }); 111 | }; 112 | } 113 | 114 | if (masks.length) { 115 | const [ , header, rest ] = /^((?:#[^\n\r\u2028\u2029]*[\n\r\u2028\u2029]*)?)([\s\S]*)$/.exec(code); 116 | wellFormednessCheck(rest, filename); 117 | 118 | // eslint-disable-next-line no-magic-numbers 119 | const maskId = `_mask${ randomBytes(16).toString('hex') }`; 120 | defineProperty( 121 | global, 122 | maskId, 123 | { 124 | value: (localRequire) => { 125 | const mask = create(null); 126 | for (const masker of masks) { 127 | masker(localRequire, mask); 128 | } 129 | return mask; 130 | }, 131 | }); 132 | 133 | const before = `with (${ maskId }(typeof require !== 'undefined' ? require : null)) {`; 134 | const after = '}'; 135 | 136 | // Preserve line numbers while creating treating rest as a FunctionBody that might have its own 137 | // PrologueDeclarations. 138 | return `${ header }${ before } return (() => { ${ rest } })(); ${ after }`; 139 | } 140 | 141 | return code; 142 | } 143 | -------------------------------------------------------------------------------- /test/external-process-test-server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | 'use strict'; 19 | 20 | /** 21 | * @fileoverview 22 | * Provides a test function compatible with ./end-to-end-common.js that 23 | * spins up a server per test case as a child process. 24 | */ 25 | 26 | const childProcess = require('child_process'); 27 | const path = require('path'); 28 | const process = require('process'); 29 | const { URL } = require('url'); 30 | 31 | const { setTimeout } = global; 32 | 33 | const shutdowns = []; 34 | process.on('SIGINT', () => { 35 | const failure = new Error('interrupted'); 36 | for (const shutdown of shutdowns) { 37 | try { 38 | shutdown(failure); 39 | } catch (exc) { 40 | console.error(exc); // eslint-disable-line no-console 41 | } 42 | } 43 | shutdowns.length = 0; 44 | }); 45 | 46 | /** 47 | * Given a root path fires up /path/to/root/scripts/run-locally.js. 48 | */ 49 | module.exports = (root) => function externalProcessTest(testFun, { quiet, isProduction }) { 50 | return function test(done) { 51 | // Each process has to spin up its own database, so this takes a bit longer 52 | // than the in-process tests. 53 | this.slow(7500); // eslint-disable-line no-invalid-this, no-magic-numbers 54 | this.timeout(15000); // eslint-disable-line no-invalid-this, no-magic-numbers 55 | 56 | const tStart = Date.now(); 57 | function logTestEvent(str) { 58 | if (!quiet) { 59 | // eslint-disable-next-line no-console 60 | console.log(`t+${ Date.now() - tStart } ${ str }`); 61 | } 62 | } 63 | 64 | let serverProcess = childProcess.spawn( 65 | path.join(root, 'scripts', 'run-locally.js'), 66 | [ 67 | // Do not append to the attacker's logfile. 68 | '--log', '/dev/null', // eslint-disable-next-line array-element-newline 69 | /* Ask server to pick a non-conflicting port. */ 70 | 'localhost', '0', 71 | ], 72 | { 73 | cwd: root, 74 | env: Object.assign( 75 | {}, 76 | // eslint-disable-next-line no-process-env 77 | process.env, 78 | { 79 | NODE_ENV: isProduction ? 'production' : 'test', 80 | UNSAFE_DISABLE_NODE_SEC_HOOKS: 'false', 81 | }), 82 | stdio: [ 'ignore', 'pipe', 'pipe' ], 83 | shell: false, 84 | }); 85 | logTestEvent(`Spawned process ${ serverProcess.pid }`); 86 | 87 | let calledDone = false; 88 | function shutdown(exc) { 89 | if (serverProcess) { 90 | try { 91 | serverProcess.stdout.destroy(); 92 | serverProcess.stderr.destroy(); 93 | if (!serverProcess.killed) { 94 | logTestEvent(`Ending process ${ serverProcess.pid }`); 95 | serverProcess.kill('SIGINT'); 96 | } 97 | serverProcess = null; 98 | } catch (shutdownFailure) { 99 | exc = exc || shutdownFailure || new Error('shutdown failed'); 100 | if (exc !== shutdownFailure) { 101 | // eslint-disable-next-line no-console 102 | console.error(shutdownFailure); 103 | } 104 | } 105 | } 106 | 107 | if (!calledDone) { 108 | calledDone = true; 109 | if (exc) { 110 | logTestEvent('Done with error'); 111 | done(exc); // eslint-disable-line callback-return 112 | } else { 113 | logTestEvent('Done without error'); 114 | done(); // eslint-disable-line callback-return 115 | } 116 | } else if (exc) { 117 | // eslint-disable-next-line no-console 118 | console.error(exc); 119 | } 120 | } 121 | shutdowns.push(shutdown); 122 | 123 | let stdout = ''; 124 | let stderr = ''; 125 | let ready = false; 126 | let baseUrl = null; 127 | 128 | function logs() { 129 | const result = { stdout, stderr }; 130 | ([ stdout, stderr ] = [ '', '' ]); 131 | return result; 132 | } 133 | serverProcess.unref(); 134 | serverProcess.on('close', () => { 135 | logTestEvent('CLOSED'); 136 | 137 | if (ready) { 138 | shutdown(); 139 | } else { 140 | shutdown(new Error(`Server did not start serving\n${ JSON.stringify(logs(), null, 2) }`)); 141 | } 142 | }); 143 | serverProcess.on('error', (exc) => done(exc || new Error('spawn failed'))); 144 | 145 | serverProcess.stdout.on('data', (chunk) => { 146 | logTestEvent(`STDOUT [[${ chunk }]]`); 147 | stdout += chunk; 148 | if (!ready) { 149 | if (baseUrl === null) { 150 | const match = /(?:^|\n)Serving from localhost:(\d+) at /.exec(stdout); 151 | if (match) { 152 | logTestEvent('GOT PORT'); 153 | const actualPort = Number(match[1]); 154 | baseUrl = new URL(`http://localhost:${ actualPort }`); 155 | } 156 | } 157 | if (/(?:^|\n)Database seeded with test data\n/.test(stdout)) { 158 | logTestEvent('READY'); 159 | ready = true; 160 | setTimeout( 161 | () => { 162 | logTestEvent('TESTING'); 163 | // Flush before starting test 164 | logs(); 165 | testFun(baseUrl, shutdown, logs); 166 | }, 167 | // eslint-disable-next-line no-magic-numbers 168 | 50); 169 | } 170 | } 171 | }); 172 | serverProcess.stderr.on('data', (chunk) => { 173 | logTestEvent(`STDERR [[${ chunk }]]`); 174 | stderr += chunk; 175 | }); 176 | }; 177 | }; 178 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | #!/bin/echo Use scripts/run-locally.js or npm start instead 2 | 3 | /** 4 | * @license 5 | * Copyright 2018 Google LLC 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * https://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | 'use strict'; 21 | 22 | /** 23 | * @fileoverview 24 | * This ensures that trusted code runs early where it has access to builtins 25 | * before any monkeypatching by the majority of unprivileged modules. 26 | */ 27 | 28 | // SENSITIVE - Shebang enables early loaded trusted hooks, and code enables security machinery. 29 | 30 | const { execSync } = require('child_process'); 31 | const fs = require('fs'); // eslint-disable-line id-length 32 | const path = require('path'); 33 | const process = require('process'); 34 | 35 | const isMain = require.main === module; 36 | 37 | require('./lib/framework/bootstrap-secure.js')(path.resolve(__dirname), isMain); 38 | 39 | const { start } = require('./lib/server.js'); 40 | const safepg = require('./lib/safe/pg.js'); 41 | const { initializeTablesWithTestData } = require('./lib/db-tables'); 42 | const { flock } = require('fs-ext'); 43 | 44 | const processInfo = { 45 | pid: process.pid, 46 | argv: [ ...process.argv ], 47 | start: Date.now(), 48 | lastcommit: (() => { 49 | try { 50 | return execSync('git rev-parse HEAD', { encoding: 'utf8' }).replace(/\s+$/, ''); 51 | } catch (exc) { 52 | return 'git failed'; 53 | } 54 | })(), 55 | localhash: (() => { 56 | try { 57 | // `git rev-parse HEAD` does not include changes to the local client. 58 | // This doesn't included unadded files, but is better. 59 | return execSync('git show --pretty=%h --abbrev=32 | shasum -ba1', { encoding: 'utf8' }).replace(/\s+$/, ''); 60 | } catch (exc) { 61 | return 'git failed'; 62 | } 63 | })(), 64 | }; 65 | 66 | if (isMain) { 67 | // Fail fast if run via `node main.js` instead of as a script with the flags from #! above. 68 | let evalWorks = true; 69 | try { 70 | eval(String(Math.random())); // eslint-disable-line no-eval 71 | } catch (evalFailed) { 72 | evalWorks = false; 73 | } 74 | 75 | const argv = [ ...process.argv ]; 76 | // './bin/node' 77 | argv.shift(); 78 | // __filename 79 | argv.shift(); 80 | 81 | let requestLogFile = './request.log'; 82 | 83 | let initDb = true; 84 | flagloop: 85 | for (;;) { 86 | switch (argv[0]) { 87 | case '--noinitdb': 88 | argv.shift(); 89 | initDb = false; 90 | break; 91 | case '--log': 92 | argv.shift(); 93 | requestLogFile = argv.shift(); 94 | break; 95 | case '--': 96 | argv.shift(); 97 | break flagloop; 98 | default: 99 | break flagloop; 100 | } 101 | } 102 | 103 | const defaultHostName = 'localhost'; 104 | const defaultPort = 8080; 105 | const defaultRootDir = path.resolve(__dirname); 106 | 107 | if (evalWorks || argv[0] === '--help') { 108 | // eslint-disable-next-line no-console 109 | console.log(`Usage: ${ __filename } [--noinitdb] [ [ []]] 110 | 111 | : The hostname the service is typically reached under. Default ${ defaultHostName } 112 | : The local port to listen on. Default ${ defaultPort } 113 | : The root directory to use for static files and stored uploads. 114 | Default "$PWD": ${ defaultRootDir } 115 | `); 116 | } else { 117 | const [ hostName = defaultHostName, port = defaultPort, rootDir = defaultRootDir ] = argv; 118 | const database = new safepg.Pool(); 119 | // eslint-disable-next-line no-magic-numbers, no-sync 120 | const requestLogFileDescriptor = requestLogFile === '-' ? 1 : fs.openSync(requestLogFile, 'a', 0o600); 121 | 122 | // Log a request so we can replay attacks. 123 | // This appends to a log file that hopefully will allow us to playback successful and 124 | // unsuccessful attacks against variant servers. 125 | const writeToPlaybackLog = (message) => { // eslint-disable-line func-style 126 | message = Object.assign(message, { processInfo }); 127 | const octets = Buffer.from(`${ JSON.stringify(message, null, 1) },\n`, 'utf8'); 128 | // 2 means lock exclusively. 129 | // We lock in case multiple server processes interleave writes to the same channel. 130 | flock(requestLogFileDescriptor, 2, () => { 131 | fs.write(requestLogFileDescriptor, octets, (exc) => { 132 | // 8 means unlock 133 | // eslint-disable-next-line no-magic-numbers 134 | flock(requestLogFileDescriptor, 8); 135 | if (exc) { 136 | console.error(exc); // eslint-disable-line no-console 137 | } 138 | }); 139 | }); 140 | }; 141 | 142 | const { stop } = start( 143 | { hostName, port, rootDir, database, writeToPlaybackLog }, 144 | (exc, actualPort) => { 145 | if (exc) { 146 | process.exitCode = 1; 147 | // eslint-disable-next-line no-console 148 | console.error(exc); 149 | } else { 150 | // Our test fixtures depends on this exact format string. 151 | // eslint-disable-next-line no-console 152 | console.log(`Serving from ${ hostName }:${ actualPort } at ${ rootDir }`); 153 | } 154 | }); 155 | 156 | // eslint-disable-next-line no-inner-declarations 157 | function tearDown() { 158 | stop(); 159 | if (requestLogFileDescriptor) { 160 | // eslint-disable-next-line no-sync 161 | fs.closeSync(requestLogFileDescriptor); 162 | } 163 | try { 164 | database.end(); 165 | } catch (exc) { 166 | // Best effort. 167 | } 168 | } 169 | process.on('SIGINT', tearDown); 170 | 171 | if (initDb) { 172 | initializeTablesWithTestData(database).then( 173 | () => { 174 | // Our test fixtures depends on this exact string. 175 | // eslint-disable-next-line no-console 176 | console.log('Database seeded with test data'); 177 | }, 178 | (exc) => { 179 | // eslint-disable-next-line no-console 180 | console.error(exc); 181 | tearDown(); 182 | }); 183 | } 184 | } 185 | } 186 | 187 | // :) 188 | -------------------------------------------------------------------------------- /lib/handlers/login.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | 'use strict'; 19 | 20 | /** 21 | * @fileoverview 22 | * Associates the current session with an account. 23 | */ 24 | 25 | require('module-keys/cjs').polyfill(module, require); 26 | 27 | const { URL } = require('url'); 28 | 29 | const safesql = require('safesql'); 30 | const bodyParser = require('body-parser'); 31 | 32 | const template = require('./login.pug'); 33 | 34 | const postdataParser = bodyParser.urlencoded({ extended: false }); 35 | 36 | // eslint-disable-next-line no-magic-numbers 37 | const STATUS_TEMPORARY_REDIRECT = 302; 38 | 39 | exports.handle = (bundle, handleError) => { 40 | const { res, req, reqUrl, database, currentAccount, sessionNonce } = bundle; 41 | 42 | // Logout before logging in 43 | if (currentAccount && currentAccount.aid !== null) { 44 | res.statusCode = STATUS_TEMPORARY_REDIRECT; 45 | // Use continue to bounce back here after logging out. 46 | const dest = new URL(`/logout?cont=${ encodeURIComponent(reqUrl.pathname + reqUrl.search) }`, reqUrl); 47 | res.setHeader('Location', dest.href); 48 | res.end(); 49 | return; 50 | } 51 | 52 | // The URL we continue to on successful login. 53 | // We thread this through from one form to another via a hidden input. 54 | function getDestinationUrl(cont) { 55 | const defaultDest = new URL('/account', reqUrl); 56 | const dest = new URL(cont || req.headers.referer || defaultDest.pathname, defaultDest); 57 | if (dest.origin === reqUrl.origin) { 58 | // No open redirector. 59 | return dest; 60 | } 61 | return defaultDest; 62 | } 63 | 64 | let formPromise = null; 65 | if (req.method === 'POST') { 66 | formPromise = new Promise((resolve, reject) => { 67 | postdataParser(req, res, (exc) => { 68 | if (exc) { 69 | reject(exc); 70 | } else { 71 | const { cont, email } = req.body; 72 | resolve({ 73 | cont, 74 | email, 75 | submitted: true, 76 | }); 77 | } 78 | }); 79 | }); 80 | } else { 81 | formPromise = Promise.resolve({ 82 | cont: reqUrl.searchParams.get('cont'), 83 | email: null, 84 | submitted: false, 85 | }); 86 | } 87 | 88 | formPromise.then( 89 | ({ email, cont, submitted }) => { 90 | const contUrl = getDestinationUrl(cont); 91 | let loginPromise = Promise.resolve(null); 92 | if (submitted && typeof email === 'string') { 93 | email = email.replace(/^\s+|\s+$/g, ''); 94 | 95 | if (email) { 96 | loginPromise = new Promise((resolve, reject) => { 97 | database.connect().then( 98 | (client) => { 99 | // Release the client if anything goes wrong. 100 | function releaseAndReject(exc) { 101 | client.release(); 102 | reject(exc); 103 | } 104 | // If there's no account id associated with the given email, we need to create an 105 | // account record, before updating the PersonalInfo table with the email. 106 | function createNewAccount() { 107 | client.query(safesql.pg`INSERT INTO Accounts DEFAULT VALUES RETURNING *`).then( 108 | (resultSet) => { 109 | const [ { aid } ] = resultSet.rows; 110 | client.query(safesql.pg`INSERT INTO PersonalInfo (aid, email) VALUES (${ aid }, ${ email })`) 111 | .then( 112 | () => { 113 | // eslint-disable-next-line no-use-before-define 114 | associateAccountWithSessionNonce(resultSet); 115 | }, 116 | releaseAndReject); 117 | }, 118 | releaseAndReject); 119 | } 120 | // Once we've got an aid we can associate it with the sessionNonce in the Sessions 121 | // table. There must be a row there because this handler wouldn't have been reached 122 | // except after fetching the current account which creates an entry in Sessions if 123 | // none exists for the sessionNonce. 124 | function associateAccountWithSessionNonce(resultSet) { 125 | if (resultSet.rowCount === 1) { 126 | const [ { aid } ] = resultSet.rows; 127 | const sessionNonceValue = require.moduleKeys.unbox(sessionNonce, () => true); 128 | client.query(safesql.pg`UPDATE SESSIONS SET aid=${ aid } WHERE sessionnonce=${ sessionNonceValue }`) 129 | .then( 130 | (updates) => { 131 | client.release(); 132 | if (updates.rowCount === 1) { 133 | resolve(aid); 134 | } else { 135 | reject(new Error(`updated ${ updates.rowCount }`)); 136 | } 137 | }, 138 | releaseAndReject); 139 | } else { 140 | createNewAccount(); 141 | } 142 | } 143 | client.query(safesql.pg`SELECT aid FROM PersonalInfo WHERE email=${ email }`).then( 144 | associateAccountWithSessionNonce, 145 | releaseAndReject); 146 | }, 147 | reject); 148 | }); 149 | } 150 | } 151 | loginPromise.then( 152 | (aid) => { 153 | if (aid === null) { 154 | res.statusCode = 200; 155 | res.end(template(Object.assign({}, bundle, { email, cont: contUrl.href }))); 156 | } else { 157 | // Redirect on successful login. 158 | res.statusCode = STATUS_TEMPORARY_REDIRECT; 159 | res.setHeader('Location', contUrl.href); 160 | res.end(); 161 | } 162 | }, 163 | handleError); 164 | }, 165 | handleError); 166 | }; 167 | -------------------------------------------------------------------------------- /lib/framework/module-stubs.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | 'use strict'; 19 | 20 | /** 21 | * @fileoverview 22 | * Stub out modules that we don't want to actually load in production. 23 | */ 24 | 25 | const { min, max } = Math; 26 | const { create } = Object; 27 | const { apply: rapply, get: rget } = Reflect; 28 | 29 | module.exports = (preventLoad) => { 30 | function stubFor(id) { 31 | const filename = require.resolve(id); 32 | if (preventLoad) { 33 | const stub = new module.constructor(filename, filename); 34 | stub.loaded = true; 35 | 36 | Object.defineProperty( 37 | require.cache, 38 | filename, 39 | { value: stub, enumerable: true }); 40 | return stub; 41 | } 42 | 43 | // eslint-disable-next-line global-require 44 | require(id); 45 | return require.cache[filename]; 46 | } 47 | 48 | // stealthy-require fails to clear the module cache which is 49 | // super obnoxious. module-keys cause that to fail noisily 50 | // which is good, but that causes modules that need it to fail. 51 | // Return a stub that doesn't do the objectionable things that 52 | // stealthy-require does. This would cause problems if we 53 | // actually needed jsdom to load scripts instead of just provide 54 | // a DOM parser for DOMPurify. 55 | // eslint-disable-next-line func-name-matching 56 | stubFor('stealthy-require').exports = function stealthyRequire( 57 | // eslint-disable-next-line no-unused-vars, id-blacklist 58 | requireCache, callback, callbackForModulesToKeep, module) { 59 | return callback(); 60 | }; 61 | 62 | // We use jsdom to parse XML. We don't want it making external 63 | // HTTP requests. XMLHttpRequest should be unnecessary but 64 | // initializing the module fails because we deny access to 65 | // "http" and "child_process". 66 | // eslint-disable-next-line func-name-matching 67 | stubFor('jsdom/lib/jsdom/living/xmlhttprequest.js').exports = function createXMLHttpRequest() { 68 | return {}; 69 | }; 70 | 71 | stubFor('jsdom/lib/jsdom/living/websockets/WebSocket-impl.js').exports = { 72 | implementation: class WebSocketImpl { 73 | }, 74 | }; 75 | 76 | stubFor('jsdom/lib/jsdom/browser/resources/resource-loader.js').exports = class ResourceLoader { 77 | // May need to stub this out so that fetch('file:') does something useful. 78 | }; 79 | 80 | // eslint-disable-next-line global-require 81 | const Promise = require('promise/lib/node-extensions.js'); 82 | 83 | /* 84 | 85 | // with arity 3 86 | 87 | const f = function (a0,a1,a2) { 88 | var self = this; 89 | return new Promise(function (rs, rj) { 90 | var res = fn.call(self,a0,a1,a2,function (err, res) { 91 | if (err) { 92 | rj(err); 93 | } else { 94 | rs(res); 95 | } 96 | }); 97 | if (res && (typeof res === "object" || typeof res === "function") && 98 | typeof res.then === "function") { 99 | rs(res); 100 | } 101 | }); 102 | }; 103 | 104 | // with fn of length 3 105 | 106 | function (a0,a1,a2) { 107 | var self = this; 108 | var args; 109 | var argLength = arguments.length; 110 | if (arguments.length > 3) { 111 | args = new Array(arguments.length + 1); 112 | for (var i = 0; 113 | i < arguments.length; 114 | i++) { 115 | args[i] = arguments[i]; 116 | } 117 | } 118 | return new Promise(function (rs, rj) { 119 | var cb = function (err, res) { 120 | if (err) { 121 | rj(err); 122 | } else { 123 | rs(res); 124 | } 125 | } 126 | ; 127 | var res; 128 | switch (argLength) { 129 | case 0:res = fn.call(self,cb); 130 | break; 131 | case 1:res = fn.call(self,a0,cb); 132 | break; 133 | case 2:res = fn.call(self,a0,a1,cb); 134 | break; 135 | case 3:res = fn.call(self,a0,a1,a2,cb); 136 | break; 137 | default:args[argLength] = cb; 138 | res = fn.apply(self, args); 139 | } 140 | if (res && (typeof res === "object" || typeof res === "function") && 141 | typeof res.then === "function") { 142 | rs(res); 143 | } 144 | }); 145 | } 146 | */ 147 | 148 | const handlerForArgumentCount = create(null); 149 | 150 | // This does what the original denodeify does but without invoking 151 | // `new Function(...)`. 152 | // It's unnecessary since delicate-globals-rewrite now allows it 153 | // to do just that, but it seems to work, and might be worth 154 | // upstreaming as versions of Node without Proxy reach end of support. 155 | function denodeify(fun, argumentCount) { 156 | if (typeof fun !== 'function') { 157 | throw new TypeError(typeof fun); 158 | } 159 | let minArity = 0; 160 | // eslint-disable-next-line no-bitwise 161 | let maxArity = (fun.length >>> 0); 162 | // eslint-disable-next-line no-bitwise 163 | if (argumentCount === (argumentCount >>> 0)) { 164 | minArity = argumentCount; 165 | maxArity = argumentCount; 166 | } 167 | 168 | const key = `${ minArity }/${ maxArity }`; 169 | 170 | if (!handlerForArgumentCount[key]) { 171 | handlerForArgumentCount[key] = { 172 | get(target, prop, receiver) { 173 | if (prop === 'length') { 174 | return maxArity; 175 | } 176 | return rget(target, prop, receiver); 177 | }, 178 | apply(target, thisArgument, argumentsList) { 179 | return new Promise((resolve, reject) => { 180 | const { length } = argumentsList; 181 | const arity = min(maxArity, max(minArity, length)); 182 | 183 | let args = null; 184 | if (arity === length) { 185 | args = argumentsList; 186 | } else { 187 | args = []; 188 | for (let i = 0; i < arity; ++i) { 189 | args[i] = argumentsList[i]; 190 | } 191 | } 192 | args[arity] = (exc, res) => { 193 | if (exc) { 194 | reject(exc); 195 | } else { 196 | resolve(res); 197 | } 198 | }; 199 | const res = rapply(target, thisArgument, args); 200 | if (res && (typeof res === 'object' || typeof res === 'function') && 201 | typeof res.then === 'function') { 202 | resolve(res); 203 | } 204 | }); 205 | }, 206 | }; 207 | } 208 | return new Proxy(fun, handlerForArgumentCount[key]); 209 | } 210 | Promise.denodeify = denodeify; 211 | }; 212 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "attack-review-testbed", 3 | "version": "1.0.0-nopublish", 4 | "description": "A testbed for github.com/nodejs/security-wg/issues/409", 5 | "main": "main.js", 6 | "scripts": { 7 | "start": "scripts/run-locally.js", 8 | "start:vuln": "vulnerable/scripts/run-locally.js", 9 | "cover": "istanbul cover _mocha", 10 | "coveralls": "npm run cover -- --report lcovonly && cat ./coverage/lcov.info | coveralls", 11 | "license": "scripts/license-check.sh", 12 | "lint": "./node_modules/.bin/eslint .", 13 | "postinstall": "scripts/postinstall.sh", 14 | "prepack": "npm run lint && npm test", 15 | "prepublishOnly": "echo This package should not be published to NPM && false", 16 | "pretest": "./scripts/build-vulnerable.sh -c", 17 | "test": "scripts/test.sh" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/mikesamuel/attack-review-testbed.git" 22 | }, 23 | "author": "@mikesamuel", 24 | "license": "Apache-2.0", 25 | "bugs": { 26 | "url": "https://github.com/mikesamuel/attack-review-testbed/issues" 27 | }, 28 | "homepage": "https://github.com/mikesamuel/attack-review-testbed#readme", 29 | "mintable": { 30 | "grants": { 31 | "web-contract-types/TrustedHTML": [ 32 | "pug-runtime-trusted-types", 33 | "./lib/handlers/account.pug", 34 | "./lib/handlers/echo.pug", 35 | "./lib/handlers/error.pug", 36 | "./lib/handlers/four-oh-four.pug", 37 | "./lib/handlers/index.pug", 38 | "./lib/handlers/login.pug", 39 | "./lib/handlers/logout.pug", 40 | "./lib/handlers/post.pug", 41 | "./lib/safe/html.js" 42 | ] 43 | }, 44 | "second": [ 45 | "safesql", 46 | "sh-template-tag", 47 | "web-contract-types" 48 | ] 49 | }, 50 | "sensitiveModules": { 51 | "child_process": { 52 | "advice": "Use safe/child_process.js instead.", 53 | "ids": [ 54 | "main.js", 55 | "lib/safe/child_process.js" 56 | ] 57 | }, 58 | "fs": { 59 | "advice": "The server has write permissions to a directory that contains static files. If you need file-system access, please contact security-oncall@. TODO: safe/fs.js will provide non-mutating file-system access", 60 | "ids": [ 61 | "clean-css/lib/reader/read-sources.js", 62 | "clean-css/lib/reader/apply-source-maps.js", 63 | "clean-css/lib/reader/load-original-sources.js", 64 | "destroy/index.js", 65 | "etag/index.js", 66 | "fd-slicer/index.js", 67 | "fs-ext/fs-ext.js", 68 | "jstransformer/index.js", 69 | "lib/server.js", 70 | "main.js", 71 | "mime/mime.js", 72 | "multiparty/index.js", 73 | "resolve/lib/async.js", 74 | "resolve/lib/node-modules-paths.js", 75 | "resolve/lib/sync.js", 76 | "pgpass/lib/index.js", 77 | "pn/fs.js", 78 | "pug-runtime/build.js", 79 | "pug/lib/index.js", 80 | "pug-load/index.js", 81 | "send/index.js", 82 | "uglify-js/tools/node.js" 83 | ] 84 | }, 85 | "http": [ 86 | "lib/server.js" 87 | ], 88 | "process": { 89 | "advice": "Process is easy to misuse. If you need access to process metadata, thread it through from main.js. Changes to the process's environment should go through the docker wrapper, and checks should go through its maintainers. Contact security-oncall@ if these options don't work for you.", 90 | "mode": "enforce", 91 | "ids": [ 92 | "main.js", 93 | "lib/framework/bootstrap-secure.js", 94 | "lib/framework/builtin-module-ids.js", 95 | "lib/framework/is-prod.js", 96 | "lib/framework/lockdown.js", 97 | "browser-process-hrtime/index.js", 98 | "clean-css/lib/reader/rewrite-url.js", 99 | "clean-css/lib/writer/source-maps.js", 100 | "debug/src/index.js", 101 | "debug/src/node.js", 102 | "depd", 103 | "iconv-lite/lib/index.js", 104 | "whatwg-encoding/node_modules/iconv-lite/lib/index.js", 105 | "jsdom/lib/jsdom/browser/Window.js", 106 | "mime/mime.js", 107 | "multiparty/index.js", 108 | "pg/lib/client.js", 109 | "pg/lib/connection-parameters.js", 110 | "pg/lib/defaults.js", 111 | "pg/lib/index.js", 112 | "pg/lib/query.js", 113 | "pgpass/lib/helper.js", 114 | "pg-pool/index.js", 115 | "resolve/lib/core.js", 116 | "uglify-js" 117 | ] 118 | }, 119 | "node_modules/pirates/lib/index.js": { 120 | "advice": "addHook affects all modules. security-oncall@ can help integrate custom loader hooks.", 121 | "ids": [ 122 | "node_modules/pug-require/index.js", 123 | "lib/framework/lockdown.js" 124 | ] 125 | }, 126 | "./lib/framework/init-hooks.js": [ 127 | "./lib/framework/bootstrap-secure.js" 128 | ], 129 | "pg": { 130 | "advice": "Use safe/pg.js instead.", 131 | "ids": [ 132 | "lib/safe/pg.js" 133 | ] 134 | }, 135 | "vm": { 136 | "mode": "enforce", 137 | "ids": [ 138 | "uglify-js/tools/node.js", 139 | "core-js/library/modules/_global.js", 140 | "depd/index.js", 141 | "pug-plugin-trusted-types/index.js", 142 | "jsdom/lib/api.js", 143 | "lodash.sortby/index.js", 144 | "nwsapi/src/nwsapi.js" 145 | ] 146 | } 147 | }, 148 | "dependencies": { 149 | "body-parser": "^1.18.3", 150 | "cookie": "^0.3.1", 151 | "csrf-crypto": "^1.0.1", 152 | "dompurify": "^1.0.8", 153 | "fs-ext": "^1.2.1", 154 | "jsdom": "^12.2.0", 155 | "mime-types": "^2.1.20", 156 | "multiparty": "^4.2.1", 157 | "node-sec-patterns": "^3.0.2", 158 | "pg": "^7.5.0", 159 | "pug": "^2.0.3", 160 | "pug-require": "^2.0.2", 161 | "safesql": "^2.0.2", 162 | "serve-static": "^1.13.2", 163 | "sh-template-tag": "^4.0.2", 164 | "tiny-relative-date": "^1.3.0", 165 | "tlds": "^1.203.1", 166 | "web-contract-types": "^2.0.2" 167 | }, 168 | "devDependencies": { 169 | "capture-console": "^1.0.1", 170 | "chai": "^4.2.0", 171 | "coveralls": "^3.0.2", 172 | "eslint": "^5.6.1", 173 | "eslint-config-strict": "^14.0.1", 174 | "istanbul": "^0.4.5", 175 | "mocha": "^5.2.0", 176 | "pre-commit": "^1.2.2", 177 | "random-number-csprng": "^1.0.2" 178 | }, 179 | "pre-commit": [ 180 | "license", 181 | "prepack" 182 | ], 183 | "eslintIgnore": [ 184 | "/bin/node.d/node/**", 185 | "/coverage/**", 186 | "**/node_modules/**" 187 | ], 188 | "eslintConfig": { 189 | "extends": [ 190 | "strict" 191 | ], 192 | "parserOptions": { 193 | "ecmaVersion": 6, 194 | "sourceType": "source", 195 | "ecmaFeatures": { 196 | "impliedStrict": false 197 | } 198 | }, 199 | "rules": { 200 | "no-confusing-arrow": [ 201 | "error", 202 | { 203 | "allowParens": true 204 | } 205 | ], 206 | "no-warning-comments": [ 207 | "error", 208 | { 209 | "terms": [ 210 | "do not submit" 211 | ] 212 | } 213 | ], 214 | "no-void": "off", 215 | "strict": [ 216 | "error", 217 | "global" 218 | ] 219 | } 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /test/sensitive-module-hook-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | 'use strict'; 19 | 20 | const { expect } = require('chai'); 21 | const { describe, it } = require('mocha'); 22 | 23 | const { makeHook } = require('../lib/framework/module-hooks/sensitive-module-hook.js'); 24 | const { runHook } = require('./run-hook.js'); 25 | 26 | describe('sensitive-module-hook', () => { 27 | const config = { 28 | 'child_process': { 29 | 'advice': 'Try to use safe/child_process instead. If that doesn\'t work, ask @security-oncall', 30 | 'ids': [ './may-use-childprocess.js' ], 31 | }, 32 | 'fs': null, 33 | 'process': { 34 | mode: 'report-only', 35 | }, 36 | './unsafe.js': './may-use-unsafe.js', 37 | }; 38 | describe('reportOnly', () => { 39 | const hook = makeHook(config, __dirname, true); 40 | describe('builtin', () => { 41 | it('allowed', () => { 42 | expect(runHook(hook, 'may-use-childprocess.js', 'child_process')) 43 | .to.deep.equal({ 44 | result: 'child_process', 45 | stdout: '', 46 | stderr: '', 47 | }); 48 | }); 49 | it('disallowed', () => { 50 | expect(runHook(hook, 'unsafe.js', 'child_process')) 51 | .to.deep.equal({ 52 | result: 'child_process', 53 | stderr: ( 54 | '../lib/framework/module-hooks/sensitive-module-hook.js:' + 55 | ' Reporting require("child_process") by unsafe.js\n\n' + 56 | '\tTry to use safe/child_process instead. If that doesn\'t work, ask @security-oncall\n'), 57 | stdout: '', 58 | }); 59 | }); 60 | it('not sensitive', () => { 61 | expect(runHook(hook, 'may-use-childprocess.js', 'path')) 62 | .to.deep.equals({ 63 | result: 'path', 64 | stderr: '', 65 | stdout: '', 66 | }); 67 | }); 68 | it('mode report-only', () => { 69 | expect(runHook(hook, 'uses-process.js', 'process')) 70 | .to.deep.equals({ 71 | result: 'process', 72 | stderr: ( 73 | '../lib/framework/module-hooks/sensitive-module-hook.js:' + 74 | ' Reporting require("process") by uses-process.js\n'), 75 | stdout: '', 76 | }); 77 | }); 78 | }); 79 | describe('user module', () => { 80 | it('allowed', () => { 81 | expect(runHook(hook, 'may-use-unsafe.js', './unsafe.js')) 82 | .to.deep.equals({ 83 | result: './unsafe.js', 84 | stderr: '', 85 | stdout: '', 86 | }); 87 | }); 88 | it('disallowed', () => { 89 | expect(runHook(hook, 'unsafe.js', './unsafe.js')) 90 | .to.deep.equals({ 91 | result: './unsafe.js', 92 | stderr: ( 93 | '../lib/framework/module-hooks/sensitive-module-hook.js:' + 94 | ' Reporting require("./unsafe.js") by unsafe.js\n'), 95 | stdout: '', 96 | }); 97 | }); 98 | it('no such file', () => { 99 | expect(() => runHook(hook, 'may-use-childprocess.js', './no-such-file.js')) 100 | .to.throw(Error, 'Cannot find module \'./no-such-file.js\''); 101 | }); 102 | it('not sensitive', () => { 103 | expect(runHook(hook, 'may-use-childprocess.js', './ok.js')) 104 | .to.deep.equals({ 105 | result: './ok.js', 106 | stdout: '', 107 | stderr: '', 108 | }); 109 | }); 110 | }); 111 | }); 112 | describe('active', () => { 113 | const hook = makeHook(config, __dirname, false); 114 | describe('builtin', () => { 115 | it('allowed', () => { 116 | expect(runHook(hook, 'may-use-childprocess.js', 'child_process')) 117 | .to.deep.equal({ 118 | result: 'child_process', 119 | stdout: '', 120 | stderr: '', 121 | }); 122 | }); 123 | it('disallowed', () => { 124 | expect(runHook(hook, 'unsafe.js', 'child_process')) 125 | .to.deep.equal({ 126 | result: require.resolve('../lib/framework/module-hooks/innocuous.js'), 127 | stderr: ( 128 | '../lib/framework/module-hooks/sensitive-module-hook.js:' + 129 | ' Blocking require("child_process") by unsafe.js\n\n' + 130 | '\tTry to use safe/child_process instead. If that doesn\'t work, ask @security-oncall\n'), 131 | stdout: '', 132 | }); 133 | }); 134 | it('disallowed no list', () => { 135 | expect(runHook(hook, 'unsafe.js', 'fs')) 136 | .to.deep.equal({ 137 | result: require.resolve('../lib/framework/module-hooks/innocuous.js'), 138 | stderr: ( 139 | '../lib/framework/module-hooks/sensitive-module-hook.js:' + 140 | ' Blocking require("fs") by unsafe.js\n'), 141 | stdout: '', 142 | }); 143 | }); 144 | it('not sensitive', () => { 145 | expect(runHook(hook, 'may-use-childprocess.js', 'path')) 146 | .to.deep.equals({ 147 | result: 'path', 148 | stderr: '', 149 | stdout: '', 150 | }); 151 | }); 152 | it('mode report-only', () => { 153 | expect(runHook(hook, 'uses-process.js', 'process')) 154 | .to.deep.equals({ 155 | result: 'process', 156 | stderr: ( 157 | '../lib/framework/module-hooks/sensitive-module-hook.js:' + 158 | ' Reporting require("process") by uses-process.js\n'), 159 | stdout: '', 160 | }); 161 | }); 162 | }); 163 | describe('user module', () => { 164 | it('allowed', () => { 165 | expect(runHook(hook, 'may-use-unsafe.js', './unsafe.js')) 166 | .to.deep.equals({ 167 | result: './unsafe.js', 168 | stderr: '', 169 | stdout: '', 170 | }); 171 | }); 172 | it('disallowed', () => { 173 | expect(runHook(hook, 'unsafe.js', './unsafe.js')) 174 | .to.deep.equals({ 175 | result: require.resolve('../lib/framework/module-hooks/innocuous.js'), 176 | stderr: ( 177 | '../lib/framework/module-hooks/sensitive-module-hook.js:' + 178 | ' Blocking require("./unsafe.js") by unsafe.js\n'), 179 | stdout: '', 180 | }); 181 | }); 182 | it('no such file', () => { 183 | expect(() => runHook(hook, 'may-use-childprocess.js', './no-such-file.js')) 184 | .to.throw(Error, 'Cannot find module \'./no-such-file.js\''); 185 | }); 186 | it('not sensitive', () => { 187 | expect(runHook(hook, 'may-use-childprocess.js', './ok.js')) 188 | .to.deep.equals({ 189 | result: './ok.js', 190 | stdout: '', 191 | stderr: '', 192 | }); 193 | }); 194 | }); 195 | }); 196 | }); 197 | -------------------------------------------------------------------------------- /lib/db-tables.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | 'use strict'; 19 | 20 | /** 21 | * @fileoverview 22 | * Utilities to set up database tables needed by handlers/*.js, and 23 | * utilities to restore to known state for tests. 24 | */ 25 | 26 | const safesql = require('safesql'); 27 | 28 | const initStmts = safesql.pg` 29 | 30 | -- Relate unique principle IDs to display info. 31 | CREATE TABLE IF NOT EXISTS Accounts ( 32 | aid serial PRIMARY KEY, 33 | displayname varchar, 34 | displaynamehtml varchar, 35 | publicurl varchar, 36 | created timestamp DEFAULT current_timestamp 37 | ); 38 | 39 | -- Relate server session nonces to Accounts. 40 | CREATE TABLE IF NOT EXISTS Sessions ( 41 | -- 256b base64 encoded 42 | sessionnonce char(44) PRIMARY KEY, 43 | aid integer, 44 | created timestamp DEFAULT current_timestamp 45 | ); 46 | 47 | -- Relates private user info to account ids. 48 | CREATE TABLE IF NOT EXISTS PersonalInfo ( 49 | aid integer UNIQUE, 50 | realname varchar, 51 | email varchar, 52 | created timestamp DEFAULT current_timestamp 53 | ); 54 | CREATE INDEX IF NOT EXISTS PersonInfo_aid ON PersonalInfo ( aid ); 55 | 56 | -- HTML snippets posted by users. 57 | -- A post is visible if it is public or its 58 | -- author and the viewer are friends. 59 | CREATE TABLE IF NOT EXISTS Posts ( 60 | pid serial PRIMARY KEY, 61 | author integer, -- JOINs on aid 62 | bodyhtml varchar NOT NULL, 63 | public bool DEFAULT false, 64 | created timestamp DEFAULT current_timestamp 65 | ); 66 | CREATE INDEX IF NOT EXISTS Posts_aid ON Posts ( author ); -- For efficient account deletion 67 | CREATE INDEX IF NOT EXISTS Posts_timestamp ON Posts ( created DESC ); -- For efficient display 68 | 69 | -- Images associated with posts. 70 | CREATE TABLE IF NOT EXISTS PostResources ( 71 | rid serial PRIMARY KEY, 72 | pid integer NOT NULL, 73 | urlpath varchar NOT NULL, 74 | created timestamp DEFAULT current_timestamp 75 | ); 76 | 77 | -- A account is always friends with themself. 78 | -- Per reciprocity, an account a is friends with account b if 79 | -- there are both entries (a, b) and (b, a) in this table. 80 | -- Every other pair of accounts are not friends :( 81 | -- An account a owns all/only entries where its aid is in leftAid. 82 | CREATE TABLE IF NOT EXISTS Friendships ( 83 | fid serial PRIMARY KEY, 84 | leftaid integer NOT NULL, -- JOINs on aid 85 | rightaid integer NOT NULL, -- JOINs on aid 86 | created timestamp DEFAULT current_timestamp 87 | ); 88 | CREATE INDEX IF NOT EXISTS Friendships_leftaid ON Friendships ( leftaid ); 89 | CREATE INDEX IF NOT EXISTS Friendships_leftaid_rightaid ON Friendships ( leftaid, rightaid ); 90 | 91 | -- Arbitrary metadata about a post. 92 | CREATE TABLE IF NOT EXISTS PostMetadata ( 93 | pid integer NOT NULL, 94 | key varchar NOT NULL, 95 | value varchar, 96 | created timestamp DEFAULT current_timestamp 97 | ); 98 | CREATE INDEX IF NOT EXISTS Postmetadata_pid ON PostMetadata ( pid ); 99 | 100 | `; 101 | 102 | const cannedData = safesql.pg` 103 | INSERT INTO Accounts 104 | ( aid, displayname, displaynamehtml, publicurl ) 105 | VALUES 106 | ( x'Ada'::int, 'Ada', NULL, NULL ), 107 | ( x'B0b'::int, NULL, 'Bob', 'javascript:alert(1)' ), 108 | ( x'Deb'::int, NULL, 'Deb', 'https://deb.example.com/' ), 109 | ( x'Fae'::int, 'Fae', 'Fae', 'mailto:fae@fae.fae' ) 110 | ; 111 | 112 | INSERT INTO PersonalInfo 113 | ( aid, realname, email ) 114 | VALUES 115 | ( x'Ada'::int, 'Ada Lovelace', 'ada@example.com' ), 116 | ( x'B0b'::int, 'Bob F. NULL', 'Bob !' ), 126 | ( x'Deb'::int, true, '2019-04-07 12:00:00', '¡Hi, all!' ), 127 | ( x'Deb'::int, false, '2019-04-08 12:00:00', 'Bob, I''m browsing via Lynx and your post isn''t Lynx-friendly.' ), 128 | ( x'Fae'::int, true, '2019-04-09 12:00:00', 'Sigh! Yet another Facebook.com knockoff without any users.' ), 129 | ( x'Fae'::int, true, '2019-04-10 12:00:00', '(It is probably insecure)' ), 130 | ( x'B0b'::int, false, '2019-04-11 12:00:00', 'You think?' ) 131 | ; 132 | 133 | INSERT INTO PostResources 134 | ( pid, urlpath ) 135 | VALUES 136 | ( 1, 'smiley.png' ) 137 | ; 138 | 139 | INSERT INTO Friendships 140 | ( leftaid, rightaid ) 141 | VALUES 142 | ( x'Ada'::int, x'Ada'::int ), 143 | ( x'Deb'::int, x'B0b'::int ), 144 | ( x'B0b'::int, x'Ada'::int ), 145 | ( x'B0b'::int, x'Deb'::int ), 146 | ( x'B0b'::int, x'Fae'::int ), 147 | ( x'Ada'::int, x'Fae'::int ), 148 | ( x'Fae'::int, x'Ada'::int ) 149 | ; 150 | `; 151 | 152 | 153 | const dropStmts = (() => { 154 | const createSql = initStmts.content; 155 | const tableNamePattern = /^CREATE (TABLE|INDEX) IF NOT EXISTS (\w+)/mg; 156 | 157 | let dropSql = null; 158 | for (let match; (match = tableNamePattern.exec(createSql));) { 159 | const [ , type, name ] = match; 160 | const canonName = name.toLowerCase(); 161 | const dropStmt = type.toUpperCase() === 'INDEX' ? 162 | safesql.pg`DROP INDEX IF EXISTS "${ canonName }"` : 163 | safesql.pg`DROP TABLE IF EXISTS "${ canonName }"`; 164 | dropSql = dropSql ? 165 | safesql.pg`${ dropSql }; 166 | ${ dropStmt }` : 167 | dropStmt; 168 | } 169 | 170 | return dropSql; 171 | })(); 172 | 173 | module.exports = { 174 | // eslint-disable-next-line no-use-before-define 175 | createTables, clearTables, initializeTablesWithTestData, 176 | }; 177 | 178 | function promiseToRun(dbPool, sql) { 179 | return new Promise((resolve, reject) => { 180 | dbPool.connect().then( 181 | (client) => { 182 | client.query(sql).then( 183 | (x) => { 184 | client.release(); 185 | resolve(x); 186 | }, 187 | (exc) => { 188 | client.release(); 189 | reject(exc); 190 | }); 191 | }, 192 | reject); 193 | }); 194 | } 195 | 196 | function createTables(dbPool) { 197 | return promiseToRun(dbPool, initStmts); 198 | } 199 | 200 | function clearTables(dbPool) { 201 | return promiseToRun(dbPool, safesql.pg`${ dropStmts };${ initStmts }`); 202 | } 203 | 204 | function initializeTablesWithTestData(dbPool) { 205 | return promiseToRun(dbPool, safesql.pg`${ dropStmts };${ initStmts };${ cannedData }`); 206 | } 207 | -------------------------------------------------------------------------------- /lib/framework/lockdown.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2018 Google LLC 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * https://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | 'use strict'; 19 | 20 | /** 21 | * @fileoverview 22 | * Prevents non-additive changes to builtins, the global object, and make 23 | * apparent when native modules like 'process' are accessed ambiently. 24 | * 25 | * This does not prevent all monkeypatching of builtins, but does mean that 26 | * console internals and core modules like 'fs' and 'path' can get a trusted 27 | * path to Function.protoype.call. 28 | * 29 | * This is best effort. If it runs before anything else freezes properties, 30 | * then it will prevent some interference between modules. 31 | */ 32 | 33 | // SENSITIVE - adds hooks that affect all loaded modules. 34 | 35 | // GUARANTEE - ability to use `new Function(...)` to load code could be 36 | // restricted to a whitelist of modules allowed to access "vm". 37 | 38 | // GUARANTEE - myFunction.call and myFunction.apply mean what developers 39 | // expect if they declared myFunction. No guarantee is made about 40 | // `.call` of apparent functions received from outside because proxies. 41 | // GUARANTEE - `String(x)` in module code that does not itself introduce 42 | // a local name `String` returns a value x such that typeof x === 'string'. 43 | 44 | // CAVEAT - We mask process in user-land modules that load via require 45 | // hooks on .js code. This does nothing to restrict global['process']. 46 | // We cannot do so without affecting builtin module code that uses 47 | // global references to `process` because invoking require('process') 48 | // would introduce chicken-egg problems. 49 | // The guarantee below is almost certainly trivially violable in ways 50 | // that we can't fix but it's worth attacker effort to enumerate some. 51 | // 52 | // Until we can really gate access to process we can't bound the amount 53 | // of code we have to check to prevent 54 | // // Attacker controlled 55 | // let x = 'process', y = 'exit'; 56 | // // Naive code 57 | // function f() { return this[x][y](); } 58 | // f(); 59 | // 60 | // Properly gating that requires changing the meaning of `this` in 61 | // non-strict function bodies which requires changing the current 62 | // realm's global object. 63 | // 64 | // TODO: experiment with changing ../../.bin/node.d/node.patch to 65 | // tweak all builtin modules that have `process` as a free variable 66 | // to start with 67 | // const process = global[Symbol.from('process')]; 68 | // and the V8 bootstrap code to attach process there. 69 | // 70 | // Actually denying process to user code would require some kind of 71 | // secret shared or closely held reference available only to builtin 72 | // code. The module loader that allows builtins access to lib/internal 73 | // modules might server as the basis for getting such a secret from V8 74 | // and conveying it to only builtin modules. 75 | // 76 | // But being able to mitigate any access via string keys would 77 | // move an unmitigated process object out of the object graph 78 | // reachable via substrings of network messages. 79 | 80 | // GUARANTEE - global access to process is restricted to a whitelist 81 | // of legacy modules. 82 | // See require('../../package.json').sensitiveModules.process 83 | 84 | 85 | // eslint-disable-next-line no-use-before-define 86 | module.exports = lockdown; 87 | 88 | const { apply } = Reflect; 89 | 90 | const globalObject = global; 91 | 92 | const { 93 | create, 94 | defineProperties, 95 | hasOwnProperty, 96 | getOwnPropertyDescriptors, 97 | getOwnPropertyNames, 98 | getOwnPropertySymbols, 99 | } = Object; 100 | 101 | const constants = require('constants'); 102 | const { exit } = require('process'); 103 | const { addHook } = require('pirates'); 104 | const delicateGlobalsRewrite = require('./delicate-globals-rewrite.js'); 105 | 106 | const languageIntrinsics = [ 107 | /* eslint-disable array-element-newline */ 108 | // Buffer and constants are actually Node builtins 109 | Array, ArrayBuffer, Boolean, Buffer, console, Date, EvalError, Error, 110 | Float32Array, Float64Array, Function, Int8Array, Int16Array, Int32Array, 111 | Intl, JSON, Object, Map, Math, Number, Promise, Proxy, RangeError, 112 | ReferenceError, Reflect, RegExp, Set, String, Symbol, SyntaxError, TypeError, 113 | URIError, Uint8Array, Uint16Array, Uint32Array, Uint8ClampedArray, WeakMap, 114 | WeakSet, global.WebAssembly, constants, 115 | /* eslint-enable array-element-newline */ 116 | ]; 117 | 118 | const doNotFreeze = { 119 | __proto__: null, 120 | Object: { 121 | __proto__: null, 122 | toString: true, 123 | }, 124 | }; 125 | 126 | const importantGlobalKeys = [ 'Infinity', 'NaN', 'global', 'GLOBAL', 'isFinite', 'isNaN', 'root', 'undefined' ]; 127 | 128 | function lockdownOwnPropertiesOf(obj, keys, whitelist) { 129 | const descriptors = getOwnPropertyDescriptors(obj); 130 | if (!keys) { 131 | // Subclassing code has to set constructor. 132 | // eslint-disable-next-line no-inner-declarations 133 | function isNonConstructorMethod(key) { 134 | return typeof descriptors[key].value === 'function' && key !== 'constructor'; 135 | } 136 | keys = [ ...getOwnPropertyNames(descriptors), ...getOwnPropertySymbols(descriptors) ] 137 | .filter(isNonConstructorMethod); 138 | } 139 | const newDescriptors = create(null); 140 | for (let i = 0, len = keys.length; i < len; ++i) { 141 | const key = keys[i]; 142 | const descriptor = descriptors[key]; 143 | if (descriptor.configurable) { 144 | descriptor.configurable = false; 145 | if (apply(hasOwnProperty, descriptor, [ 'value' ]) && 146 | !(whitelist && whitelist[key] === true)) { 147 | descriptor.writable = false; 148 | } 149 | newDescriptors[key] = descriptor; 150 | try { 151 | delete obj[key]; 152 | } catch (exc) { 153 | // best effort 154 | } 155 | } 156 | } 157 | defineProperties(obj, newDescriptors); 158 | } 159 | 160 | function lockdownIntrinsics() { 161 | const globalsToPin = []; 162 | // Pin core prototype methods in place. 163 | for (const languageIntrinsic of languageIntrinsics) { 164 | const whitelist = languageIntrinsic.name ? doNotFreeze[languageIntrinsic.name] : null; 165 | lockdownOwnPropertiesOf(languageIntrinsic, null, whitelist); 166 | if (typeof languageIntrinsic === 'function' && languageIntrinsic.prototype) { 167 | lockdownOwnPropertiesOf(languageIntrinsic.prototype, null, whitelist); 168 | } 169 | if (typeof languageIntrinsic === 'function' && languageIntrinsic.name && 170 | globalObject[languageIntrinsic.name] === languageIntrinsic) { 171 | globalsToPin[globalsToPin.length] = languageIntrinsic.name; 172 | } 173 | } 174 | return globalsToPin; 175 | } 176 | 177 | function maskDelicateGlobals() { 178 | addHook(delicateGlobalsRewrite, { exts: [ '.js' ], ignoreNodeModules: false }); 179 | } 180 | 181 | let lockeddown = false; 182 | function lockdown() { 183 | if (lockeddown) { 184 | return; 185 | } 186 | lockeddown = true; 187 | 188 | try { 189 | const globalsToPin = [ ...importantGlobalKeys, ...lockdownIntrinsics() ]; 190 | 191 | // Pin important globals in place. 192 | lockdownOwnPropertiesOf(globalObject, globalsToPin); 193 | 194 | maskDelicateGlobals(); 195 | } catch (exc) { 196 | try { 197 | // eslint-disable-next-line no-console 198 | console.error(exc); 199 | } finally { 200 | // Fail hard. 201 | exit(1); 202 | // TODO: reallyExit? 203 | } 204 | } 205 | } 206 | --------------------------------------------------------------------------------