diff --git a/demo/esm/index.mjs b/demo/esm/index.mjs index 20b04df59d..ff77ab3c20 100644 --- a/demo/esm/index.mjs +++ b/demo/esm/index.mjs @@ -8,7 +8,7 @@ const engine = new Liquid({ // layout files for `{% layout %}` layouts: process.cwd() + '/layouts', // partial files for `{% include %}` and `{% render %}` - partials: process.cwd() + '/partials' + partials: [process.cwd() + '/partials', 'node_modules'] }) const ctx = { diff --git a/demo/esm/test.sh b/demo/esm/test.sh index ed85b9cd7c..3beedd07da 100755 --- a/demo/esm/test.sh +++ b/demo/esm/test.sh @@ -1,3 +1,3 @@ -set -ex +set -e npm start | grep 'LiquidJS Demo' diff --git a/demo/express/test.sh b/demo/express/test.sh index 66c15deac8..ebf5f976d8 100755 --- a/demo/express/test.sh +++ b/demo/express/test.sh @@ -1,4 +1,4 @@ -set -x +set -e LOG_FILE=$(mktemp) npm start > $LOG_FILE 2>&1 & diff --git a/demo/nodejs/test.sh b/demo/nodejs/test.sh index 03beaae99b..0593783faa 100755 --- a/demo/nodejs/test.sh +++ b/demo/nodejs/test.sh @@ -1,3 +1,3 @@ -set -ex +set -e npm start | grep 'NodeJS Demo for LiquidJS' diff --git a/demo/template/test.sh b/demo/template/test.sh index 80d5ffd388..efcab23018 100755 --- a/demo/template/test.sh +++ b/demo/template/test.sh @@ -1,3 +1,3 @@ -set -ex +set -e npm start | grep '\[11:8] {{ todo }}' diff --git a/demo/typescript/test.sh b/demo/typescript/test.sh index 59bedb0328..6bf336a256 100755 --- a/demo/typescript/test.sh +++ b/demo/typescript/test.sh @@ -1,3 +1,3 @@ -set -ex +set -e npm run build && npm start | grep 'TypeScript Demo for LiquidJS' diff --git a/demo/webpack/test.sh b/demo/webpack/test.sh index 7dc39dc1b3..106350db14 100755 --- a/demo/webpack/test.sh +++ b/demo/webpack/test.sh @@ -1,4 +1,4 @@ -set -ex +set -e npm run build npm start | grep 'Webpack Demo for LiquidJS' diff --git a/docs/source/tutorials/render-file.md b/docs/source/tutorials/render-file.md index 30c417888f..0f636852ff 100644 --- a/docs/source/tutorials/render-file.md +++ b/docs/source/tutorials/render-file.md @@ -45,26 +45,20 @@ It can be a string-typed path (see above example), or a list of root directories ```javascript var engine = new Liquid({ - root: ['views/', 'views/partials/'], + root: ['views/'], + partials: ['views/partials/'], + layouts: ['views/layouts/'], extname: '.liquid' }); ``` {% note tip Relative Paths %}Relative paths in root will be resolved against cwd().{% endnote %} -When `{% raw %}{% render "foo" %}{% endraw %}` is rendered or `liquid.renderFile('foo')` is called, the following files will be looked up and the first existing file will be used: +- When `parse()`, `render()` functions are called, for example `liquid.renderFile('foo')`, templates under `root` will be looked up. +- When a partial is requested, for example `{% raw %}{% render "foo" %}{% endraw %}`, templates under `partials` will be looked up. +- When a layout is requested, for example `{% raw %}{% layout "foo" %}{% endraw %}`, templates under `layouts` will be looked up. -- `cwd()`/views/foo.liquid -- `cwd()`/views/partials/foo.liquid - -If none of the above files exists, an `ENOENT` error will be thrown. Here's a demo for Node.js: [demo/nodejs](https://github.com/harttle/liquidjs/tree/master/demo/nodejs). - -When LiquidJS is used in browser, say current location is , only the first `root` will be used and the file to be fetched is: - -- - -If fetch fails, a 404/500 error or network failures for example, an `ENOENT` error will be thrown. -Here's a demo for browsers: [demo/browser](https://github.com/harttle/liquidjs/tree/master/demo/browser). +When LiquidJS is used in browser, the paths will be resolved based on current location. Here's a demo for browsers: [demo/browser](https://github.com/harttle/liquidjs/tree/master/demo/browser). ## Abstract File System diff --git a/src/fs/loader.ts b/src/fs/loader.ts index cb59041b1e..62c061a1a6 100644 --- a/src/fs/loader.ts +++ b/src/fs/loader.ts @@ -43,25 +43,26 @@ export class Loader { public * candidates (file: string, dirs: string[], currentFile?: string, enforceRoot?: boolean) { const { fs, extname } = this.options - if (this.shouldLoadRelative(file) && currentFile) { - const referenced = fs.resolve(this.dirname(currentFile), file, extname) + const isAllowed = (filepath: string) => { + if (!enforceRoot) return true for (const dir of dirs) { - if (!enforceRoot || this.contains(dir, referenced)) { - // the relatively referenced file is within one of root dirs - yield referenced - break - } + if (this.contains(dir, filepath)) return true } + return false + } + + if (this.shouldLoadRelative(file) && currentFile) { + const referenced = fs.resolve(this.dirname(currentFile), file, extname) + if (isAllowed(referenced)) yield referenced } for (const dir of dirs) { const referenced = fs.resolve(dir, file, extname) - if (!enforceRoot || this.contains(dir, referenced)) { - yield referenced - } + if (isAllowed(referenced)) yield referenced } + if (fs.fallback !== undefined) { const filepath = fs.fallback(file) - if (filepath !== undefined) yield filepath + if (filepath !== undefined && isAllowed(filepath)) yield filepath } } diff --git a/test/e2e/issues.spec.ts b/test/e2e/issues.spec.ts index 65ed98536d..c5243fe662 100644 --- a/test/e2e/issues.spec.ts +++ b/test/e2e/issues.spec.ts @@ -1,4 +1,6 @@ import { TopLevelToken, TagToken, Tokenizer, Context, Liquid, Drop, toValueSync, LiquidError, IfTag } from '../..' +import { spawnSync } from 'child_process' +import { resolve as resolvePath } from 'path' const LiquidUMD = require('../../dist/liquid.browser.umd.js').Liquid describe('Issues', function () { @@ -173,6 +175,24 @@ describe('Issues', function () { const html = await engine.render(tpl, { my_variable: 'foo' }) expect(html).toBe('CONTENT for /tmp/prefix/foo-bar/suffix') }) + it('should prevent path traversal in dynamic include with restricted root, #851', () => { + const projectRoot = resolvePath(__dirname, '../..') + const poc = ` + const { Liquid } = require('./dist/liquid.node.js'); + const e = new Liquid({ root: ['/tmp'], partials: ['/tmp'], dynamicPartials: true }); + e.parseAndRender('{% include page %}', { page: '../../../etc/passwd' }) + .then(() => { console.log('OK'); }) + .catch(err => { console.error('ERR:' + err.message); process.exit(1); }); + ` + const result = spawnSync( + process.execPath, + ['-e', poc], + { cwd: projectRoot, encoding: 'utf8' } + ) + + expect(result.status).not.toBe(0) + expect(result.stderr).toContain('Failed to lookup') + }) it('Implement liquid/echo tags #428', () => { const template = `{%- liquid for value in array diff --git a/test/tsconfig.json b/test/tsconfig.json index 83e0923c2b..8c81c92cc3 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -10,8 +10,7 @@ "allowSyntheticDefaultImports": true, "resolveJsonModule": true, "downlevelIteration": true, - "strict": true, - "suppressImplicitAnyIndexErrors": true + "strict": true }, "all": true }