VYPR
High severityNVD Advisory· Published Mar 10, 2026· Updated Mar 11, 2026

liquidjs has a path traversal fallback vulnerability

CVE-2026-30952

Description

liquidjs is a Shopify / GitHub Pages compatible template engine in pure JavaScript. Prior to 10.25.0, the layout, render, and include tags allow arbitrary file access via absolute paths (either as string literals or through Liquid variables, the latter require dynamicPartials: true, which is the default). This poses a security risk when malicious users are allowed to control the template content or specify the filepath to be included as a Liquid variable. This vulnerability is fixed in 10.25.0.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

In LiquidJS before 10.25.0, the layout, render, and include tags permit arbitrary file access via absolute paths due to an unsafe fallback to require.resolve, allowing path traversal.

Vulnerability

LiquidJS is a Shopify/GitHub Pages compatible template engine in pure JavaScript. Prior to version 10.25.0, the layout, render, and include tags allow arbitrary file access via absolute paths. The root cause is that the tag resolution logic uses require.resolve() as a fallback without restricting which directories can be accessed, effectively bypassing the configured root directory for partials. This is a classic path traversal vulnerability [4].

Exploitation

An attacker can exploit this vulnerability by controlling the template content (e.g., via user input) or by specifying the filepath as a Liquid variable when dynamicPartials is set to true (the default). No authentication is required if the attacker can inject templates. The attack is simple: include an absolute path like /etc/passwd in a {% render %} or {% include %} tag, causing the server to read and output the file [1][2].

Impact

Successful exploitation allows an attacker to read arbitrary files on the server's filesystem, leading to disclosure of sensitive information such as configuration files, credentials, or source code. This can severely compromise the confidentiality of the application and its environment [4].

Mitigation

The vulnerability is fixed in version 10.25.0. The patch implements proper directory containment checks in the fallback resolution logic [3]. As a temporary workaround, users may modify the distributed files to enforce root directory checks or override the fs option provided to the Liquid engine to disable the fallback entirely [4].

AI Insight generated on May 18, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
liquidjsnpm
< 10.25.010.25.0

Affected products

1
  • harttle/liquidjsv5
    Range: < 10.25.0

Patches

1
3cd024d652dc

fix: path traversal vulnerability, #851 (#855)

https://github.com/harttle/liquidjsYang JunMar 7, 2026via ghsa
11 files changed · +47 33
  • demo/esm/index.mjs+1 1 modified
    @@ -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 = {
    
  • demo/esm/test.sh+1 1 modified
    @@ -1,3 +1,3 @@
    -set -ex
    +set -e
     
     npm start | grep 'LiquidJS Demo'
    
  • demo/express/test.sh+1 1 modified
    @@ -1,4 +1,4 @@
    -set -x
    +set -e
     
     LOG_FILE=$(mktemp)
     npm start > $LOG_FILE 2>&1 &
    
  • demo/nodejs/test.sh+1 1 modified
    @@ -1,3 +1,3 @@
    -set -ex
    +set -e
     
     npm start | grep 'NodeJS Demo for LiquidJS'
    
  • demo/template/test.sh+1 1 modified
    @@ -1,3 +1,3 @@
    -set -ex
    +set -e
     
     npm start | grep '\[11:8] {{ todo }}'
    
  • demo/typescript/test.sh+1 1 modified
    @@ -1,3 +1,3 @@
    -set -ex
    +set -e
     
     npm run build && npm start | grep 'TypeScript Demo for LiquidJS'
    
  • demo/webpack/test.sh+1 1 modified
    @@ -1,4 +1,4 @@
    -set -ex
    +set -e
     
     npm run build
     npm start | grep 'Webpack Demo for LiquidJS'
    
  • docs/source/tutorials/render-file.md+7 13 modified
    @@ -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 <code>root</code> will be resolved against <code>cwd()</code>.{% 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 <https://example.com/bar/index.html>, only the first `root` will be used and the file to be fetched is:
    -
    -- <https://example.com/bar/foo.liquid>
    -
    -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
     
    
  • src/fs/loader.ts+12 11 modified
    @@ -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
         }
       }
     
    
  • test/e2e/issues.spec.ts+20 0 modified
    @@ -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
    
  • test/tsconfig.json+1 2 modified
    @@ -10,8 +10,7 @@
         "allowSyntheticDefaultImports": true,
         "resolveJsonModule": true,
         "downlevelIteration": true,
    -    "strict": true,
    -    "suppressImplicitAnyIndexErrors": true
    +    "strict": true
       },
       "all": true
     }
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

6

News mentions

0

No linked articles in our index yet.