-
Notifications
You must be signed in to change notification settings - Fork 2k
feat(fuzzing): Fuzz runtime options to verify security boundaries #2120
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 3 commits
3acb1ff
ed81f97
d69f675
98112e8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| {{constructor.constructor}} | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| kw1="{{" | ||
| kw2="}}" | ||
| kw3="constructor" | ||
| kw4="prototype" | ||
| kw5="name" | ||
| kw6=".__proto__" | ||
| kw7=".constructor" |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,45 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const Handlebars = require('../lib/index.js'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @param {Buffer} data | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| module.exports.fuzz = function (data) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (data.length < 1) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const flags = data[0]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const template = data.slice(1).toString(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Use the first byte to fuzz runtime options | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const runtimeOptions = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| allowProtoPropertiesByDefault: !!(flags & 1), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| allowProtoMethodsByDefault: !!(flags & 2), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| allowedProtoMethods: flags & 4 ? { constructor: true } : undefined, // Also fuzz unsafe constructor access? maybe dangerous high noise. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // We keep constructor check strictly out of the fuzzing for now unless we want to catch if it leaks DESPITE being false? | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Defaults blacklist constructor. "allowProtoMethodsByDefault" should NOT unblock constructor unless "allowedProtoMethods" explicitly allows it. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+15
to
+18
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| allowProtoMethodsByDefault: !!(flags & 2), | |
| allowedProtoMethods: flags & 4 ? { constructor: true } : undefined, // Also fuzz unsafe constructor access? maybe dangerous high noise. | |
| }; | |
| // We keep constructor check strictly out of the fuzzing for now unless we want to catch if it leaks DESPITE being false? | |
| // Defaults blacklist constructor. "allowProtoMethodsByDefault" should NOT unblock constructor unless "allowedProtoMethods" explicitly allows it. | |
| allowProtoMethodsByDefault: !!(flags & 2) | |
| }; | |
| // We keep constructor check strictly out of the fuzzing for now unless we want to catch if it leaks DESPITE being false? | |
| // Defaults blacklist constructor. "allowProtoMethodsByDefault" should NOT unblock constructor unless "allowedProtoMethods" explicitly allows it. | |
| // Defaults blacklist constructor. "allowProtoMethodsByDefault" should NOT unblock constructor unless "allowedProtoMethods" explicitly allows it. |
Copilot
AI
Feb 27, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current detection logic only looks for function-like signatures (e.g., [native code], function ...). A bypass that exposes a dangerous non-function prototype value (e.g., __proto__ returning an object) would likely render as something like [object Object] and won’t be flagged. To better match the project’s security expectations (see spec/security.js “dangerous properties”), consider explicitly checking for any successful resolution of those dangerous property names (e.g., non-empty output) rather than only function substrings.
| const render = Handlebars.compile(template); | |
| const result = render({}, runtimeOptions); | |
| // Check if we managed to access a prototype property that returns a function signature | |
| if ( | |
| result.includes('[native code]') || | |
| result.includes('function Object') || | |
| result.includes('function Function') || | |
| result.includes('function anonymous') || | |
| (result.includes('function') && !template.includes('function')) | |
| // Properties that are considered dangerous when accessed via prototypes. | |
| const dangerousPropertyNames = ['__proto__', 'constructor', 'prototype']; | |
| const render = Handlebars.compile(template); | |
| const result = render({}, runtimeOptions); | |
| // Check if we managed to access a prototype property that returns a function signature | |
| const accessedDangerousProperty = | |
| dangerousPropertyNames.some(function (name) { | |
| return template.includes(name); | |
| }) && | |
| typeof result === 'string' && | |
| result.trim().length > 0; | |
| if ( | |
| result.includes('[native code]') || | |
| result.includes('function Object') || | |
| result.includes('function Function') || | |
| result.includes('function anonymous') || | |
| (result.includes('function') && !template.includes('function')) || | |
| accessedDangerousProperty |
Copilot
AI
Feb 27, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The broad catch currently suppresses all non-detection exceptions, which can hide real crashes/bugs during fuzzing (TypeError, RangeError, etc.). Consider only ignoring expected template parse/compile errors (e.g., Handlebars.Exception from invalid templates) and rethrowing everything else so Jazzer can surface genuine failures.
| error.message && | |
| error.message.startsWith('Prototype Access Detected') | |
| ) { | |
| throw error; | |
| } | |
| // Ignore other errors | |
| error && | |
| error.message && | |
| error.message.startsWith('Prototype Access Detected') | |
| ) { | |
| throw error; | |
| } | |
| // Ignore expected Handlebars template parse/compile errors, but surface all others. | |
| if (error instanceof Handlebars.Exception) { | |
| return; | |
| } | |
| // Re-throw unexpected errors so the fuzzer (e.g., Jazzer) can detect real crashes/bugs. | |
| throw error; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This corpus seed doesn’t include the leading “flags” byte consumed by
fuzz_compile.js(const flags = data[0]; const template = data.slice(1)...). As written, the first{becomes the flags value and the template is missing its first character, so the seed won’t exercise{{constructor.constructor}}as intended. Either prefix the seed with a real flags byte (e.g., 0x00) or change the harness to encode flags in-band (e.g., a textual header) so corpus entries remain valid templates.