diff --git a/docs/bug-detectors.md b/docs/bug-detectors.md index 4c9d5c9e0..def3baa96 100644 --- a/docs/bug-detectors.md +++ b/docs/bug-detectors.md @@ -100,9 +100,11 @@ using Jest in `.jazzerjsrc.json`: ## Remote Code Execution -Hooks the `eval` and `Function` functions and reports a finding if the fuzzer -was able to pass a special string to `eval` and to the function body of -`Function`. +Installs a canary getter on `globalThis` and hooks the `eval` and `Function` +functions. The before-hooks guide the fuzzer toward injecting the canary +identifier into code strings. A finding is reported when dynamically compiled +code accesses the canary property, avoiding false positives from the identifier +merely appearing inside string literals. _Disable with:_ `--disableBugDetectors=remote-code-execution` in CLI mode; or when using Jest in `.jazzerjsrc.json`: diff --git a/packages/bug-detectors/internal/remote-code-execution.ts b/packages/bug-detectors/internal/remote-code-execution.ts index fbe724e31..a26728d38 100644 --- a/packages/bug-detectors/internal/remote-code-execution.ts +++ b/packages/bug-detectors/internal/remote-code-execution.ts @@ -14,33 +14,70 @@ * limitations under the License. */ +import type { Context } from "vm"; + import { + getJazzerJsGlobal, guideTowardsContainment, reportAndThrowFinding, } from "@jazzer.js/core"; -import { callSiteId, registerBeforeHook } from "@jazzer.js/hooking"; +import { registerBeforeHook } from "@jazzer.js/hooking"; + +const CANARY_NAME = "jaz_zer"; + +function createCanaryDescriptor(canaryName: string): PropertyDescriptor { + return { + get() { + reportAndThrowFinding( + "Remote Code Execution\n" + + ` attacker-controlled code accessed globalThis.${canaryName}`, + ); + }, + enumerable: false, + configurable: false, + }; +} + +function installCanaryIfMissing( + target: object, + canaryName: string, + descriptor: PropertyDescriptor, +): void { + if (Object.getOwnPropertyDescriptor(target, canaryName)) { + return; + } + Object.defineProperty(target, canaryName, descriptor); +} + +// The canary should be present in both globals used by Jazzer.js: +// - globalThis in CLI mode +// - vmContext in Jest mode +const canaryDescriptor = createCanaryDescriptor(CANARY_NAME); +installCanaryIfMissing(globalThis, CANARY_NAME, canaryDescriptor); + +const vmContext = getJazzerJsGlobal("vmContext"); +if (vmContext) { + installCanaryIfMissing(vmContext, CANARY_NAME, canaryDescriptor); +} -const targetString = "jaz_zer"; +// Guidance: before-hooks steer the fuzzer toward getting the canary name into +// eval/Function bodies. A finding is reported when compiled code reads it. registerBeforeHook( "eval", "", false, - function beforeEvalHook(_thisPtr: unknown, params: string[], hookId: number) { + function beforeEvalHook( + _thisPtr: unknown, + params: unknown[], + hookId: number, + ) { const code = params[0]; - // This check will prevent runtime TypeErrors should the user decide to call Function with - // non-string arguments. - // noinspection SuspiciousTypeOfGuard - if (typeof code === "string" && code.includes(targetString)) { - reportAndThrowFinding( - "Remote Code Execution\n" + ` using eval:\n '${code}'`, - ); + // eval with non-string arguments is a no-op (returns the argument as-is), + // so guidance is only meaningful for actual strings. + if (typeof code === "string") { + guideTowardsContainment(code, CANARY_NAME, hookId); } - - // Since we do not hook eval using the hooking framework, we have to recompute the - // call site ID on every call to eval. This shouldn't be an issue, because eval is - // considered evil and should not be called too often, or even better -- not at all! - guideTowardsContainment(code, targetString, hookId); }, ); @@ -50,22 +87,27 @@ registerBeforeHook( false, function beforeFunctionHook( _thisPtr: unknown, - params: string[], + params: unknown[], hookId: number, ) { - if (params.length > 0) { - const functionBody = params[params.length - 1]; + if (params.length === 0) return; - // noinspection SuspiciousTypeOfGuard - if (typeof functionBody === "string") { - if (functionBody.includes(targetString)) { - reportAndThrowFinding( - "Remote Code Execution\n" + - ` using Function:\n '${functionBody}'`, - ); - } - guideTowardsContainment(functionBody, targetString, hookId); - } + // The Function constructor coerces every argument to string via ToString(). + // Template engines (e.g. Handlebars) pass non-string objects like SourceNode + // whose toString() yields executable code. Coerce here to match V8's + // behavior so guidance works for those cases too. + const functionBody = params[params.length - 1]; + if (functionBody == null) return; + + let functionBodySource: string; + try { + functionBodySource = String(functionBody); + } catch { + // toString() would also throw inside the Function constructor, so + // no code will be compiled, no RCE risk, no guidance needed. + return; } + + guideTowardsContainment(functionBodySource, CANARY_NAME, hookId); }, ); diff --git a/tests/bug-detectors/remote-code-execution.test.js b/tests/bug-detectors/remote-code-execution.test.js index 9d251eb3a..134ac5cfb 100644 --- a/tests/bug-detectors/remote-code-execution.test.js +++ b/tests/bug-detectors/remote-code-execution.test.js @@ -36,120 +36,130 @@ beforeEach(() => { }); describe("CLI", () => { - describe("eval ()", () => { - it("Invocation without error", () => { - const fuzzTest = fuzzTestBuilder - .fuzzEntryPoint("invocationWithoutError") - .build() - .execute(); - expect(fuzzTest.stdout).toContain(okMessage); - }); - - it("Direct invocation", () => { - const fuzzTest = fuzzTestBuilder - .fuzzEntryPoint("directInvocation") - .sync(true) - .build(); - expect(() => { - fuzzTest.execute(); - }).toThrow(FuzzingExitCode); - expect(fuzzTest.stderr).toContain(findingMessage); - }); + const cliCanaryFindingCases = { + eval: [ + { + title: "Accesses canary", + entryPoint: "evalAccessesCanary", + }, + { + title: "Indirect accesses canary", + entryPoint: "evalIndirectAccessesCanary", + }, + { + title: "Comma operator accesses canary", + entryPoint: "evalCommaOperatorAccessesCanary", + }, + { + title: "Optional chaining accesses canary", + entryPoint: "evalOptionalChainingAccessesCanary", + }, + ], + functionConstructor: [ + { + title: "Accesses canary", + entryPoint: "functionAccessesCanary", + }, + { + title: "Accesses canary with new", + entryPoint: "functionNewAccessesCanary", + }, + { + title: "With arg accesses canary", + entryPoint: "functionWithArgAccessesCanary", + }, + { + title: "String-coercible body accesses canary", + entryPoint: "functionStringCoercibleAccessesCanary", + }, + ], + }; - it("Indirect invocation", () => { - const fuzzTest = fuzzTestBuilder - .fuzzEntryPoint("indirectInvocation") - .sync(true) - .build(); - expect(() => { - fuzzTest.execute(); - }).toThrow(FuzzingExitCode); - expect(fuzzTest.stderr).toContain(findingMessage); - }); + const cliNoFindingCases = { + eval: [ + { + title: "Safe code - no error", + entryPoint: "evalSafeCode", + }, + { + title: "Target in string literal - no error", + entryPoint: "evalTargetInStringLiteral", + }, + ], + functionConstructor: [ + { + title: "Safe code - no error", + entryPoint: "functionSafeCode", + }, + { + title: "Safe code with new - no error", + entryPoint: "functionSafeCodeNew", + }, + { + title: "Target in arg name - no error", + entryPoint: "functionTargetInArgName", + }, + { + title: "Target in string literal - no error", + entryPoint: "functionTargetInStringLiteral", + }, + { + title: "String-coercible safe body - no error", + entryPoint: "functionStringCoercibleSafe", + }, + ], + }; - it("Indirect invocation using comma operator", () => { - const fuzzTest = fuzzTestBuilder - .fuzzEntryPoint("indirectInvocationUsingCommaOperator") - .sync(true) - .build(); - expect(() => { - fuzzTest.execute(); - }).toThrow(FuzzingExitCode); - expect(fuzzTest.stderr).toContain(findingMessage); - }); + describe("eval", () => { + for (const testCase of cliCanaryFindingCases.eval) { + it(testCase.title, () => { + const fuzzTest = fuzzTestBuilder + .fuzzEntryPoint(testCase.entryPoint) + .sync(true) + .build(); + expect(() => { + fuzzTest.execute(); + }).toThrow(FuzzingExitCode); + expect(fuzzTest.stderr).toContain(findingMessage); + }); + } - it("Indirect invocation through optional chaining", () => { - const fuzzTest = fuzzTestBuilder - .fuzzEntryPoint("indirectInvocationThroughOptionalChaining") - .sync(true) - .build(); - expect(() => { - fuzzTest.execute(); - }).toThrow(FuzzingExitCode); - expect(fuzzTest.stderr).toContain(findingMessage); - }); + for (const testCase of cliNoFindingCases.eval) { + it(testCase.title, () => { + const fuzzTest = fuzzTestBuilder + .fuzzEntryPoint(testCase.entryPoint) + .sync(true) + .build() + .execute(); + expect(fuzzTest.stdout).toContain(okMessage); + }); + } }); describe("Function constructor", () => { - it("Invocation without error, without explicit constructor", () => { - const fuzzTest = fuzzTestBuilder - .fuzzEntryPoint("functionNoErrorNoConstructor") - .sync(true) - .build() - .execute(); - expect(fuzzTest.stdout).toContain(okMessage); - }); - - it("Invocation without error", () => { - const fuzzTest = fuzzTestBuilder - .fuzzEntryPoint("functionNoErrorWithConstructor") - .sync(true) - .build() - .execute(); - expect(fuzzTest.stdout).toContain(okMessage); - }); + for (const testCase of cliCanaryFindingCases.functionConstructor) { + it(testCase.title, () => { + const fuzzTest = fuzzTestBuilder + .fuzzEntryPoint(testCase.entryPoint) + .sync(true) + .build(); + expect(() => { + fuzzTest.execute(); + }).toThrow(FuzzingExitCode); + expect(fuzzTest.stderr).toContain(findingMessage); + }); + } - it("Direct invocation", () => { - const fuzzTest = fuzzTestBuilder - .fuzzEntryPoint("functionError") - .sync(true) - .build(); - expect(() => { - fuzzTest.execute(); - }).toThrow(FuzzingExitCode); - expect(fuzzTest.stderr).toContain(findingMessage); - }); - - it("Direct invocation using new", () => { - const fuzzTest = fuzzTestBuilder - .fuzzEntryPoint("functionErrorNew") - .sync(true) - .build(); - expect(() => { - fuzzTest.execute(); - }).toThrow(FuzzingExitCode); - expect(fuzzTest.stderr).toContain(findingMessage); - }); - - it("Target string in variable name - no error", () => { - const fuzzTest = fuzzTestBuilder - .fuzzEntryPoint("functionWithArgNoError") - .sync(true) - .build() - .execute(); - expect(fuzzTest.stdout).toContain(okMessage); - }); - - it("With error - target string in last arg", () => { - const fuzzTest = fuzzTestBuilder - .fuzzEntryPoint("functionWithArgError") - .sync(true) - .build(); - expect(() => { - fuzzTest.execute(); - }).toThrow(FuzzingExitCode); - expect(fuzzTest.stderr).toContain(findingMessage); - }); + for (const testCase of cliNoFindingCases.functionConstructor) { + it(testCase.title, () => { + const fuzzTest = fuzzTestBuilder + .fuzzEntryPoint(testCase.entryPoint) + .sync(true) + .build() + .execute(); + expect(fuzzTest.stdout).toContain(okMessage); + }); + } it("Function.prototype should still exist", () => { const fuzzTest = fuzzTestBuilder @@ -163,133 +173,89 @@ describe("CLI", () => { }); describe("Jest", () => { - describe("eval", () => { - it("Direct invocation", () => { - const fuzzTest = fuzzTestBuilder - .dryRun(false) - .jestTestFile("tests.fuzz.js") - .jestTestName("eval Direct invocation$") - .build(); - expect(() => { - fuzzTest.execute(); - }).toThrow(JestRegressionExitCode); - expect(fuzzTest.stderr).toContain(findingMessage); - }); + const jestCanaryFindingCases = { + eval: [ + "eval Accesses canary$", + "eval Indirect accesses canary$", + "eval Comma operator accesses canary$", + "eval Optional chaining accesses canary$", + ], + functionConstructor: [ + "Function Accesses canary$", + "Function Accesses canary with new$", + "Function With arg accesses canary$", + "Function String-coercible body accesses canary$", + ], + }; - it("Indirect invocation", () => { - const fuzzTest = fuzzTestBuilder - .dryRun(false) - .jestTestFile("tests.fuzz.js") - .jestTestName("eval Indirect invocation$") - .build(); - expect(() => { - fuzzTest.execute(); - }).toThrow(JestRegressionExitCode); - expect(fuzzTest.stderr).toContain(findingMessage); - }); - - it("Indirect invocation using comma operator", () => { - const fuzzTest = fuzzTestBuilder - .dryRun(false) - .jestTestFile("tests.fuzz.js") - .jestTestName("eval Indirect invocation using comma operator$") - .build(); - expect(() => { - fuzzTest.execute(); - }).toThrow(JestRegressionExitCode); - expect(fuzzTest.stderr).toContain(findingMessage); - }); + const jestNoFindingCases = { + eval: [ + "eval Safe code - no error$", + "eval Target in string literal - no error$", + ], + functionConstructor: [ + "Function Safe code - no error$", + "Function Safe code with new - no error$", + "Function Target in arg name - no error$", + "Function Target in string literal - no error$", + "Function String-coercible safe body - no error$", + ], + }; - it("Indirect invocation using optional chaining", () => { - const fuzzTest = fuzzTestBuilder - .dryRun(false) - .verbose(true) - .jestTestFile("tests.fuzz.js") - .jestTestName("eval Indirect invocation through optional chaining$") - .build(); - expect(() => { - fuzzTest.execute(); - }).toThrow(JestRegressionExitCode); - expect(fuzzTest.stderr).toContain(findingMessage); - }); + describe("eval", () => { + for (const jestTestName of jestCanaryFindingCases.eval) { + it(jestTestName.replace("$", ""), () => { + const fuzzTest = fuzzTestBuilder + .dryRun(false) + .jestTestFile("tests.fuzz.js") + .jestTestName(jestTestName) + .build(); + expect(() => { + fuzzTest.execute(); + }).toThrow(JestRegressionExitCode); + expect(fuzzTest.stderr).toContain(findingMessage); + }); + } - it("No error with absence of the target string", () => { - const fuzzTest = fuzzTestBuilder - .dryRun(false) - .jestTestFile("tests.fuzz.js") - .jestTestName("eval No error$") - .build() - .execute(); - expect(fuzzTest.stdout).toContain(okMessage); - }); + for (const jestTestName of jestNoFindingCases.eval) { + it(jestTestName.replace("$", ""), () => { + const fuzzTest = fuzzTestBuilder + .dryRun(false) + .jestTestFile("tests.fuzz.js") + .jestTestName(jestTestName) + .build() + .execute(); + expect(fuzzTest.stdout).toContain(okMessage); + }); + } }); describe("Function constructor", () => { - it("No error", () => { - const fuzzTest = fuzzTestBuilder - .dryRun(false) - .jestTestFile("tests.fuzz.js") - .jestTestName("Function No error$") - .build(); - fuzzTest.execute(); - expect(fuzzTest.stdout).toContain(okMessage); - }); + for (const jestTestName of jestCanaryFindingCases.functionConstructor) { + it(jestTestName.replace("$", ""), () => { + const fuzzTest = fuzzTestBuilder + .dryRun(false) + .jestTestFile("tests.fuzz.js") + .jestTestName(jestTestName) + .build(); + expect(() => { + fuzzTest.execute(); + }).toThrow(JestRegressionExitCode); + expect(fuzzTest.stderr).toContain(findingMessage); + }); + } - it("No error with constructor", () => { - const fuzzTest = fuzzTestBuilder - .dryRun(false) - .jestTestFile("tests.fuzz.js") - .jestTestName("Function No error with constructor$") - .build(); - fuzzTest.execute(); - expect(fuzzTest.stdout).toContain(okMessage); - }); - - it("With error", () => { - const fuzzTest = fuzzTestBuilder - .dryRun(false) - .jestTestFile("tests.fuzz.js") - .jestTestName("Function With error$") - .build(); - expect(() => { - fuzzTest.execute(); - }).toThrow(JestRegressionExitCode); - expect(fuzzTest.stderr).toContain(findingMessage); - }); - - it("With error with constructor", () => { - const fuzzTest = fuzzTestBuilder - .dryRun(false) - .jestTestFile("tests.fuzz.js") - .jestTestName("Function With error with constructor$") - .build(); - expect(() => { - fuzzTest.execute(); - }).toThrow(JestRegressionExitCode); - expect(fuzzTest.stderr).toContain(findingMessage); - }); - - it("Variable name containing target string should not throw", () => { - const fuzzTest = fuzzTestBuilder - .dryRun(false) - .jestTestFile("tests.fuzz.js") - .jestTestName("Function Target string in variable name - no error$") - .build() - .execute(); - expect(fuzzTest.stdout).toContain(okMessage); - }); - - it("With variable, body contains target string - should throw", () => { - const fuzzTest = fuzzTestBuilder - .dryRun(false) - .jestTestFile("tests.fuzz.js") - .jestTestName("Function With error - target string in last arg$") - .build(); - expect(() => { - fuzzTest.execute(); - }).toThrow(JestRegressionExitCode); - expect(fuzzTest.stderr).toContain(findingMessage); - }); + for (const jestTestName of jestNoFindingCases.functionConstructor) { + it(jestTestName.replace("$", ""), () => { + const fuzzTest = fuzzTestBuilder + .dryRun(false) + .jestTestFile("tests.fuzz.js") + .jestTestName(jestTestName) + .build() + .execute(); + expect(fuzzTest.stdout).toContain(okMessage); + }); + } it("Function.prototype should still exist", () => { const fuzzTest = fuzzTestBuilder diff --git a/tests/bug-detectors/remote-code-execution/fuzz.js b/tests/bug-detectors/remote-code-execution/fuzz.js index daab6b959..ac8e0cdc6 100644 --- a/tests/bug-detectors/remote-code-execution/fuzz.js +++ b/tests/bug-detectors/remote-code-execution/fuzz.js @@ -14,57 +14,84 @@ * limitations under the License. */ -const printOkMessage = "console.log('can be called just fine')"; +// --- eval: should report (canary is accessed) --- -// eval -module.exports.invocationWithoutError = function (data) { - eval("const a = 10; const b = 20;" + printOkMessage); +module.exports.evalAccessesCanary = function (data) { + eval("jaz_zer()"); }; -module.exports.directInvocation = function (data) { - eval("const jaz_zer = 10;"); +module.exports.evalIndirectAccessesCanary = function (data) { + const e = eval; + e("jaz_zer()"); }; -module.exports.indirectInvocation = function (data) { - const a = eval; - a("const jaz_zer = 10;"); +module.exports.evalCommaOperatorAccessesCanary = function (data) { + (0, eval)("jaz_zer()"); }; -module.exports.indirectInvocationUsingCommaOperator = function (data) { - (0, eval)("const jaz_zer = 10;"); +module.exports.evalOptionalChainingAccessesCanary = function (data) { + eval?.("jaz_zer()"); }; -module.exports.indirectInvocationThroughOptionalChaining = function (data) { - eval?.("const jaz_zer = 10;"); +// --- eval: should NOT trigger --- + +module.exports.evalSafeCode = function (data) { + eval("const a = 10; const b = 20; console.log('can be called just fine')"); +}; + +module.exports.evalTargetInStringLiteral = function (data) { + // The target string appears inside a quoted string literal — not code. + // This is the kind of false positive the canary approach eliminates. + eval("const x = 'jaz_zer'; console.log('can be called just fine')"); +}; + +// --- Function: should report (canary is accessed) --- + +module.exports.functionAccessesCanary = function (data) { + Function("jaz_zer()")(); }; -// Function -module.exports.functionNoErrorNoConstructor = function (data) { - Function("const a = 10; const b = 20;" + printOkMessage)(); +module.exports.functionNewAccessesCanary = function (data) { + new Function("jaz_zer()")(); }; -module.exports.functionNoErrorWithConstructor = function (data) { - const fn = new Function("const a = 10; const b = 20;" + printOkMessage); - fn(); +module.exports.functionWithArgAccessesCanary = function (data) { + new Function("a", "jaz_zer()")("_"); +}; + +module.exports.functionStringCoercibleAccessesCanary = function (data) { + // Simulates template engines (e.g. Handlebars) passing a SourceNode-like + // object whose toString() yields code that accesses the canary. + const body = { toString: () => "jaz_zer()" }; + Function(body)(); +}; + +// --- Function: should NOT trigger --- + +module.exports.functionSafeCode = function (data) { + Function("console.log('can be called just fine')")(); }; -module.exports.functionError = function (data) { - Function("const jaz_zer = 10;"); +module.exports.functionSafeCodeNew = function (data) { + new Function("console.log('can be called just fine')")(); }; -module.exports.functionErrorNew = function (data) { - new Function("const jaz_zer = 10;")(); +module.exports.functionTargetInArgName = function (data) { + // "jaz_zer" as a parameter name, not in the body — not code execution. + new Function("jaz_zer", "console.log('can be called just fine')")("_"); }; -module.exports.functionWithArgNoError = function (data) { - new Function( - "jaz_zer", - "const foo = 10; console.log('Function can be called just fine')", - )("_"); +module.exports.functionTargetInStringLiteral = function (data) { + // The target string appears inside a quoted string literal in the body. + // This is the Handlebars false-positive scenario: the canary is never triggered. + new Function("const x = 'jaz_zer'; console.log('can be called just fine')")(); }; -module.exports.functionWithArgError = function (data) { - new Function("foo", "const jaz_zer = 10;")("_"); +module.exports.functionStringCoercibleSafe = function (data) { + const body = { + toString: () => "console.log('can be called just fine')", + }; + Function(body)(); }; module.exports.functionPrototypeExists = function (data) { diff --git a/tests/bug-detectors/remote-code-execution/tests.fuzz.js b/tests/bug-detectors/remote-code-execution/tests.fuzz.js index a7a7a3a88..8c2873d26 100644 --- a/tests/bug-detectors/remote-code-execution/tests.fuzz.js +++ b/tests/bug-detectors/remote-code-execution/tests.fuzz.js @@ -17,49 +17,66 @@ const tests = require("./fuzz"); describe("eval", () => { - it.fuzz("No error", (data) => { - tests.invocationWithoutError(data); + it.fuzz("Accesses canary", (data) => { + tests.evalAccessesCanary(data); }); - it.fuzz("Direct invocation", (data) => { - tests.directInvocation(data); + it.fuzz("Indirect accesses canary", (data) => { + tests.evalIndirectAccessesCanary(data); }); - it.fuzz("Indirect invocation", (data) => { - tests.indirectInvocation(data); + it.fuzz("Comma operator accesses canary", (data) => { + tests.evalCommaOperatorAccessesCanary(data); }); - it.fuzz("Indirect invocation using comma operator", (data) => { - tests.indirectInvocationUsingCommaOperator(data); + it.fuzz("Optional chaining accesses canary", (data) => { + tests.evalOptionalChainingAccessesCanary(data); }); - it.fuzz("Indirect invocation through optional chaining", (data) => { - tests.indirectInvocationThroughOptionalChaining(data); + it.fuzz("Safe code - no error", (data) => { + tests.evalSafeCode(data); + }); + + it.fuzz("Target in string literal - no error", (data) => { + tests.evalTargetInStringLiteral(data); }); }); describe("Function", () => { - it.fuzz("No error", (data) => { - tests.functionNoErrorNoConstructor(); + it.fuzz("Accesses canary", (data) => { + tests.functionAccessesCanary(data); }); - it.fuzz("No error with constructor", (data) => { - tests.functionNoErrorWithConstructor(data); + + it.fuzz("Accesses canary with new", (data) => { + tests.functionNewAccessesCanary(data); + }); + + it.fuzz("With arg accesses canary", (data) => { + tests.functionWithArgAccessesCanary(data); + }); + + it.fuzz("String-coercible body accesses canary", (data) => { + tests.functionStringCoercibleAccessesCanary(data); + }); + + it.fuzz("Safe code - no error", (data) => { + tests.functionSafeCode(data); }); - it.fuzz("With error", (data) => { - tests.functionError(data); + it.fuzz("Safe code with new - no error", (data) => { + tests.functionSafeCodeNew(data); }); - it.fuzz("With error with constructor", (data) => { - tests.functionErrorNew(data); + it.fuzz("Target in arg name - no error", (data) => { + tests.functionTargetInArgName(data); }); - it.fuzz("Target string in variable name - no error", (data) => { - tests.functionWithArgNoError(data); + it.fuzz("Target in string literal - no error", (data) => { + tests.functionTargetInStringLiteral(data); }); - it.fuzz("With error - target string in last arg", (data) => { - tests.functionWithArgError(data); + it.fuzz("String-coercible safe body - no error", (data) => { + tests.functionStringCoercibleSafe(data); }); it.fuzz("Function.prototype still exists", (data) => {