Skip to content

Commit 3cd024d

Browse files
fix: path traversal vulnerability, #851 (#855)
* Fix Path Traversal fallback * Update loader.ts Fixed nested * Update loader.ts padding fix * refactor: reuse root enforcing * docs: update test case and docs --------- Co-authored-by: MorielHarush <93482738+MorielHarush@users.noreply.github.com>
1 parent 85233e0 commit 3cd024d

11 files changed

Lines changed: 47 additions & 33 deletions

File tree

demo/esm/index.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const engine = new Liquid({
88
// layout files for `{% layout %}`
99
layouts: process.cwd() + '/layouts',
1010
// partial files for `{% include %}` and `{% render %}`
11-
partials: process.cwd() + '/partials'
11+
partials: [process.cwd() + '/partials', 'node_modules']
1212
})
1313

1414
const ctx = {

demo/esm/test.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
set -ex
1+
set -e
22

33
npm start | grep 'LiquidJS Demo'

demo/express/test.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
set -x
1+
set -e
22

33
LOG_FILE=$(mktemp)
44
npm start > $LOG_FILE 2>&1 &

demo/nodejs/test.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
set -ex
1+
set -e
22

33
npm start | grep 'NodeJS Demo for LiquidJS'

demo/template/test.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
set -ex
1+
set -e
22

33
npm start | grep '\[11:8] {{ todo }}'

demo/typescript/test.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
set -ex
1+
set -e
22

33
npm run build && npm start | grep 'TypeScript Demo for LiquidJS'

demo/webpack/test.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
set -ex
1+
set -e
22

33
npm run build
44
npm start | grep 'Webpack Demo for LiquidJS'

docs/source/tutorials/render-file.md

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -45,26 +45,20 @@ It can be a string-typed path (see above example), or a list of root directories
4545

4646
```javascript
4747
var engine = new Liquid({
48-
root: ['views/', 'views/partials/'],
48+
root: ['views/'],
49+
partials: ['views/partials/'],
50+
layouts: ['views/layouts/'],
4951
extname: '.liquid'
5052
});
5153
```
5254

5355
{% note tip Relative Paths %}Relative paths in <code>root</code> will be resolved against <code>cwd()</code>.{% endnote %}
5456

55-
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:
57+
- When `parse()`, `render()` functions are called, for example `liquid.renderFile('foo')`, templates under `root` will be looked up.
58+
- When a partial is requested, for example `{% raw %}{% render "foo" %}{% endraw %}`, templates under `partials` will be looked up.
59+
- When a layout is requested, for example `{% raw %}{% layout "foo" %}{% endraw %}`, templates under `layouts` will be looked up.
5660

57-
- `cwd()`/views/foo.liquid
58-
- `cwd()`/views/partials/foo.liquid
59-
60-
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).
61-
62-
When LiquidJS is used in browser, say current location is <https://example.com/bar/index.html>, only the first `root` will be used and the file to be fetched is:
63-
64-
- <https://example.com/bar/foo.liquid>
65-
66-
If fetch fails, a 404/500 error or network failures for example, an `ENOENT` error will be thrown.
67-
Here's a demo for browsers: [demo/browser](https://github.com/harttle/liquidjs/tree/master/demo/browser).
61+
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).
6862

6963
## Abstract File System
7064

src/fs/loader.ts

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -43,25 +43,26 @@ export class Loader {
4343

4444
public * candidates (file: string, dirs: string[], currentFile?: string, enforceRoot?: boolean) {
4545
const { fs, extname } = this.options
46-
if (this.shouldLoadRelative(file) && currentFile) {
47-
const referenced = fs.resolve(this.dirname(currentFile), file, extname)
46+
const isAllowed = (filepath: string) => {
47+
if (!enforceRoot) return true
4848
for (const dir of dirs) {
49-
if (!enforceRoot || this.contains(dir, referenced)) {
50-
// the relatively referenced file is within one of root dirs
51-
yield referenced
52-
break
53-
}
49+
if (this.contains(dir, filepath)) return true
5450
}
51+
return false
52+
}
53+
54+
if (this.shouldLoadRelative(file) && currentFile) {
55+
const referenced = fs.resolve(this.dirname(currentFile), file, extname)
56+
if (isAllowed(referenced)) yield referenced
5557
}
5658
for (const dir of dirs) {
5759
const referenced = fs.resolve(dir, file, extname)
58-
if (!enforceRoot || this.contains(dir, referenced)) {
59-
yield referenced
60-
}
60+
if (isAllowed(referenced)) yield referenced
6161
}
62+
6263
if (fs.fallback !== undefined) {
6364
const filepath = fs.fallback(file)
64-
if (filepath !== undefined) yield filepath
65+
if (filepath !== undefined && isAllowed(filepath)) yield filepath
6566
}
6667
}
6768

test/e2e/issues.spec.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { TopLevelToken, TagToken, Tokenizer, Context, Liquid, Drop, toValueSync, LiquidError, IfTag } from '../..'
2+
import { spawnSync } from 'child_process'
3+
import { resolve as resolvePath } from 'path'
24
const LiquidUMD = require('../../dist/liquid.browser.umd.js').Liquid
35

46
describe('Issues', function () {
@@ -173,6 +175,24 @@ describe('Issues', function () {
173175
const html = await engine.render(tpl, { my_variable: 'foo' })
174176
expect(html).toBe('CONTENT for /tmp/prefix/foo-bar/suffix')
175177
})
178+
it('should prevent path traversal in dynamic include with restricted root, #851', () => {
179+
const projectRoot = resolvePath(__dirname, '../..')
180+
const poc = `
181+
const { Liquid } = require('./dist/liquid.node.js');
182+
const e = new Liquid({ root: ['/tmp'], partials: ['/tmp'], dynamicPartials: true });
183+
e.parseAndRender('{% include page %}', { page: '../../../etc/passwd' })
184+
.then(() => { console.log('OK'); })
185+
.catch(err => { console.error('ERR:' + err.message); process.exit(1); });
186+
`
187+
const result = spawnSync(
188+
process.execPath,
189+
['-e', poc],
190+
{ cwd: projectRoot, encoding: 'utf8' }
191+
)
192+
193+
expect(result.status).not.toBe(0)
194+
expect(result.stderr).toContain('Failed to lookup')
195+
})
176196
it('Implement liquid/echo tags #428', () => {
177197
const template = `{%- liquid
178198
for value in array

0 commit comments

Comments
 (0)