Skip to content

feat(fuzzing): Fuzz runtime options to verify security boundaries#2120

Open
biplavbarua wants to merge 4 commits intohandlebars-lang:masterfrom
biplavbarua:feature/fuzz-runtime-options
Open

feat(fuzzing): Fuzz runtime options to verify security boundaries#2120
biplavbarua wants to merge 4 commits intohandlebars-lang:masterfrom
biplavbarua:feature/fuzz-runtime-options

Conversation

@biplavbarua
Copy link

Enhances the fuzzer to toggle allowProtoPropertiesByDefault and allowProtoMethodsByDefault using the first byte of input. This allows the fuzzer to verify that prototype access controls work as expected even when default protections are relaxed, catching potential bypasses in non-default configurations.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a Jazzer.js-based fuzzing harness to exercise Handlebars runtime prototype-access controls under different allowProto*ByDefault configurations, aiming to catch security boundary regressions in non-default runtime options.

Changes:

  • Add Jazzer.js dev dependencies and an npm run fuzz script.
  • Introduce a fuzz target (fuzz_compile.js) that compiles/renders templates while toggling runtime proto-access options based on input.
  • Add a fuzzing dictionary and an initial corpus seed focused on prototype/constructor access patterns.

Reviewed changes

Copilot reviewed 4 out of 5 changed files in this pull request and generated 5 comments.

File Description
package.json Adds Jazzer.js tooling deps and a fuzz script entrypoint.
fuzz/fuzz_compile.js New fuzz target that compiles/renders templates while varying proto-access runtime options and checks for suspected prototype access.
fuzz/dict Adds libFuzzer-style dictionary tokens to bias mutations toward Handlebars/prototype keywords.
fuzz/corpus/prototype_pollution_seed Adds an initial seed intended to probe constructor/prototype access paths.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@@ -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.
},
"devDependencies": {
"@definitelytyped/dtslint": "^0.0.100",
"@jazzer.js/core": "^2.1.0",
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.

@jazzer.js/core is marked as deprecated in the current lockfile metadata; adding it as a direct devDependency may have long-term maintenance/CI implications. If there’s a maintained Jazzer.js package/version to target, consider switching to that (or pinning/documenting why this deprecated dependency is still required).

Suggested change
"@jazzer.js/core": "^2.1.0",
"@jazzer.js/core": "2.1.0",

Copilot uses AI. Check for mistakes.
Comment on lines +15 to +20
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.
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.
Comment on lines +23 to +32
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.
Comment on lines +38 to +43
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants