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
}