Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions fuzz/corpus/prototype_pollution_seed
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{constructor.constructor}}
Copy link

Copilot AI Feb 27, 2026

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.

Copilot uses AI. Check for mistakes.
7 changes: 7 additions & 0 deletions fuzz/dict
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"
45 changes: 45 additions & 0 deletions fuzz/fuzz_compile.js
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
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

runtimeOptions.allowedProtoMethods is being set to { constructor: true } when flags & 4, which explicitly whitelists constructor access. This contradicts the nearby comment about keeping constructor checks out of fuzzing and will also defeat the intended security-boundary assertions (since the harness will be asking Handlebars to allow the most dangerous method). Consider removing this flag entirely (or only fuzzing allowProto*ByDefault), and keep allowedProtoMethods undefined unless you’re specifically testing whitelisting semantics in a separate target.

Suggested change
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 uses AI. Check for mistakes.
// Let's stick to fuzzing the ByDefault options first.

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'))
Copy link

Copilot AI Feb 27, 2026

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.

Suggested change
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 uses AI. Check for mistakes.
) {
throw new Error('Prototype Access Detected: ' + result);
}
} catch (error) {
if (
error.message &&
error.message.startsWith('Prototype Access Detected')
) {
throw error;
}
// Ignore other errors
Copy link

Copilot AI Feb 27, 2026

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.

Suggested change
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;

Copilot uses AI. Check for mistakes.
}
};
Loading