Monitor vulnerabilities like this one.
Sign up free to get alerted when software you use is affected.
8.7
LiquidJS allows malicious file access through absolute paths
GHSA-wmfp-5q7x-987x
CVE-2026-30952
GHSA-wmfp-5q7x-987x
Summary
LiquidJS, a templating engine, can be tricked into accessing arbitrary files on the system due to a path traversal vulnerability. This can happen when user-controlled data is used to specify file paths in templates. To fix this, update to version 10.25.0 of LiquidJS or configure the engine to limit file access by overriding the fs implementation or modifying the file in build time.
What to do
- Update liquidjs to version 10.25.0.
Affected software
| Vendor | Product | Affected versions | Fix available |
|---|---|---|---|
| – | liquidjs | <= 10.25.0 | 10.25.0 |
Original title
liquidjs has a path traversal fallback vulnerability
Original description
### Impact
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.
### Patches
The root cause is LiquidJS allows `require.resolve()` as fallback but doesn't limit the directories it can resolve to. The issue is fixed via [#855](https://github.com/harttle/liquidjs/pull/855) and published version 10.25.0 on npm.
### Workarounds
#### Change the files in build time
In build time, through Shell script or Webpack `string-replace-loader`, change the file content of correxponding file (depending on your package `type`, for CommonJS it's `dist/liquid.node.js`) under `dist/`,
```diff
if (fs.fallback !== undefined) {
const filepath = fs.fallback(file)
- if (filepath !== undefined) yield filepath
+ if (filepath !== undefined) {
+ for (const dir of dirs) {
+ if (!enforceRoot || this.contains(dir, filepath)) {
+ yield filepath
+ break
+ }
+ }
}
}
```
#### Overriding by `fs` LiquidJS option
Adding a [`fs` option](https://liquidjs.com/api/interfaces/FS.html) to override the [default `fs` implementation](https://github.com/harttle/liquidjs/blob/1b85fdaa9c535021f7030a239a64003af26d31b5/src/fs/fs-impl.ts#L36-L40):
```javascript
const { statSync, readFileSync, promises: { stat, readFile } } = require('fs')
const { resolve, extname, dirname, sep } = require('path')
const fs = {
exists: async (fp) => { try { await stat(fp); return true; } catch { return false } },
existsSync: (fp) => { try { statSync(fp); return true } catch { return false } },
resolve: (root, file, ext) => resolve(root, file + (extname(file) ? '' : ext)),
contains: (root, file) => {
const r = resolve(root)
return file.startsWith(r.endsWith(sep) ? r : r + sep)
},
readFile: (fp) => readFile(fp, 'utf8'),
readFileSync: (fp) => readFileSync(fp, 'utf8'),
fallback: () => undefined,
dirname,
sep
};
const engine = new Liquid({ fs })
```
### References
Discussions: https://github.com/harttle/liquidjs/pull/851
Code fix: https://github.com/harttle/liquidjs/pull/855
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.
### Patches
The root cause is LiquidJS allows `require.resolve()` as fallback but doesn't limit the directories it can resolve to. The issue is fixed via [#855](https://github.com/harttle/liquidjs/pull/855) and published version 10.25.0 on npm.
### Workarounds
#### Change the files in build time
In build time, through Shell script or Webpack `string-replace-loader`, change the file content of correxponding file (depending on your package `type`, for CommonJS it's `dist/liquid.node.js`) under `dist/`,
```diff
if (fs.fallback !== undefined) {
const filepath = fs.fallback(file)
- if (filepath !== undefined) yield filepath
+ if (filepath !== undefined) {
+ for (const dir of dirs) {
+ if (!enforceRoot || this.contains(dir, filepath)) {
+ yield filepath
+ break
+ }
+ }
}
}
```
#### Overriding by `fs` LiquidJS option
Adding a [`fs` option](https://liquidjs.com/api/interfaces/FS.html) to override the [default `fs` implementation](https://github.com/harttle/liquidjs/blob/1b85fdaa9c535021f7030a239a64003af26d31b5/src/fs/fs-impl.ts#L36-L40):
```javascript
const { statSync, readFileSync, promises: { stat, readFile } } = require('fs')
const { resolve, extname, dirname, sep } = require('path')
const fs = {
exists: async (fp) => { try { await stat(fp); return true; } catch { return false } },
existsSync: (fp) => { try { statSync(fp); return true } catch { return false } },
resolve: (root, file, ext) => resolve(root, file + (extname(file) ? '' : ext)),
contains: (root, file) => {
const r = resolve(root)
return file.startsWith(r.endsWith(sep) ? r : r + sep)
},
readFile: (fp) => readFile(fp, 'utf8'),
readFileSync: (fp) => readFileSync(fp, 'utf8'),
fallback: () => undefined,
dirname,
sep
};
const engine = new Liquid({ fs })
```
### References
Discussions: https://github.com/harttle/liquidjs/pull/851
Code fix: https://github.com/harttle/liquidjs/pull/855
ghsa CVSS4.0
8.7
Vulnerability type
CWE-22
Path Traversal
- https://github.com/harttle/liquidjs/security/advisories/GHSA-wmfp-5q7x-987x
- https://github.com/harttle/liquidjs/pull/851
- https://github.com/harttle/liquidjs/pull/855
- https://github.com/harttle/liquidjs/commit/3cd024d652dc883c46307581e979fe32302ad...
- https://github.com/advisories/GHSA-wmfp-5q7x-987x
- https://github.com/harttle/liquidjs Product
- https://nvd.nist.gov/vuln/detail/CVE-2026-30952
Published: 10 Mar 2026 · Updated: 13 Mar 2026 · First seen: 10 Mar 2026