OpenZeppelin Contracts Wizard has Code Injection in Generated Hardhat and Foundry Tests via Unsanitized opts.name / opts.uri
Description
OpenZeppelin Contracts Wizard would inject unescaped user input into generated Hardhat and Foundry test files, allowing arbitrary code execution upon test run.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
OpenZeppelin Contracts Wizard would inject unescaped user input into generated Hardhat and Foundry test files, allowing arbitrary code execution upon test run.
Vulnerability
In @openzeppelin/wizard versions prior to 0.10.9, the zipHardhat and zipFoundry functions interpolated user-supplied strings (opts.name, opts.uri) directly into Hardhat (test/test.ts) and Foundry (test/.t.sol) example test files without escaping. This code injection vulnerability allowed an attacker to craft input that would break out of the string literal context and be parsed as executable code when a developer ran npm test or forge test on a generated project. The exported functions zipHardhat and zipFoundry are not part of the documented public API of @openzeppelin/wizard [1][3].
Exploitation
An attacker who controls the opts.name or opts.uri parameters passed to zipHardhat or zipFoundry could embed malicious JavaScript or Solidity code within those strings. Upon generation of the project template, the crafted input would appear as code in the test file. When a developer downloads and runs the tests (e.g., npm test or forge test), the injected code executes in the context of the developer's environment, with the privileges of the user running the test framework [1][3].
Impact
Successful exploitation leads to arbitrary code execution on the developer's machine. An attacker could steal credentials, exfiltrate source code, modify project files, or deploy malware. The full scope of compromise includes confidentiality, integrity, and availability of the affected system. No authentication is required beyond the ability to supply the vulnerable parameters to the wizard functions [1][3][4].
Mitigation
The vulnerability is fixed in @openzeppelin/wizard@0.10.9. Users of the hosted wizard at https://wizard.openzeppelin.com are not affected because the site has been redeployed with the fix. Users of the package via its documented public API are also not affected. Callers who directly invoke zipHardhat or zipFoundry with externally controlled strings must upgrade to version 0.10.9 or later. No known workarounds exist for the vulnerable functions [1][2][3][4].
AI Insight generated on Jun 11, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
1- Range: <= 0.10.8
Patches
1ec12c44f8d9eMerge commit from fork
11 files changed · +727 −7
.changeset/lemon-islands-rest.md+5 −0 added@@ -0,0 +1,5 @@ +--- +'@openzeppelin/wizard': patch +--- + +Escape `opts.name` and `opts.uri` when generating Hardhat and Foundry test files. Only affects callers of `zipHardhat` / `zipFoundry`; these functions are not part of the documented public API.
packages/core/solidity/src/utils/sanitize.test.ts+45 −0 modified@@ -38,6 +38,51 @@ test('stringifyUnicodeSafe', t => { expected: 'unicode"MyTokeć"', description: 'should handle string with mixed ASCII and unicode characters', }, + { + input: 'Path\\file', + expected: '"Path\\\\file"', + description: 'should escape backslash in ASCII branch', + }, + { + input: 'é\\");', + expected: 'unicode"é\\\\\\");"', + description: 'should escape backslash and quote together in unicode branch (no breakout)', + }, + { + input: '😀', + expected: 'unicode"😀"', + description: 'should pass non-BMP characters through raw (no surrogate escapes)', + }, + { + input: '\x7F', + expected: 'unicode"\x7F"', + description: 'should pass DEL through raw (allowed by unicode literal grammar)', + }, + { + input: '\x00', + expected: 'unicode"\x00"', + description: 'should pass NUL through raw (allowed by unicode literal grammar)', + }, + { + input: '\x08', + expected: 'unicode"\x08"', + description: 'should pass backspace through raw (no Solidity \\b escape exists)', + }, + { + input: 'a\nb', + expected: 'unicode"a\\nb"', + description: 'should escape LF (excluded from raw unicode literal body)', + }, + { + input: 'a\rb', + expected: 'unicode"a\\rb"', + description: 'should escape CR (excluded from raw unicode literal body)', + }, + { + input: 'a\x0bb', + expected: 'unicode"a\\x0bb"', + description: 'should escape vertical tab (Solidity lexer treats it as a line terminator)', + }, ]; for (const { input, expected, description } of cases) {
packages/core/solidity/src/utils/sanitize.ts+21 −3 modified@@ -1,6 +1,24 @@ +/** + * Returns a Solidity string literal whose decoded value is `str`. + * + * The form is chosen from Solidity's grammar (see + * https://docs.soliditylang.org/en/latest/grammar.html): + * + * - For input that fits the regular `"..."` literal (DoubleQuotedPrintable = + * printable ASCII 0x20-0x7E minus `"` and `\`), only `"` and `\` need escaping. + * - Otherwise, `unicode"..."` is used. The grammar excludes `"`, CR, LF, `\` + * from the raw body; vertical tab (0x0B) is escaped too because Solidity's + * lexer treats it as a line terminator. Non-BMP characters pass through as + * raw UTF-8 (no surrogate escapes). + */ export function stringifyUnicodeSafe(str: string): string { + const needsUnicode = /[^\x20-\x7E]/u.test(str); + if (!needsUnicode) { + return `"${str.replace(/[\\"]/g, '\\$&')}"`; + } // eslint-disable-next-line no-control-regex - const containsUnicode = /[^\x00-\x7F]/.test(str); - - return containsUnicode ? `unicode"${str.replace(/"/g, '\\"')}"` : JSON.stringify(str); + const body = str.replace(/[\\"\r\n\x0b]/gu, c => + c === '"' ? '\\"' : c === '\\' ? '\\\\' : c === '\n' ? '\\n' : c === '\r' ? '\\r' : '\\x0b', + ); + return `unicode"${body}"`; }
packages/core/solidity/src/zip-foundry.test.ts+30 −0 modified@@ -147,6 +147,36 @@ test.serial('custom transparent, managed', async t => { await runTest(c, t, opts); }); +test.serial('erc20 name containing double quotes', async t => { + const opts: GenericOptions = { + kind: 'ERC20', + name: 'My "Token"', + symbol: 'MTK', + }; + const c = buildERC20(opts); + await runTest(c, t, opts); +}); + +test.serial('erc1155 uri containing double quotes', async t => { + const opts: GenericOptions = { + kind: 'ERC1155', + name: 'My Token', + uri: 'https://example.com/"id"/{id}', + }; + const c = buildERC1155(opts); + await runTest(c, t, opts); +}); + +test.serial('erc20 name containing unicode, backslash, and double quotes', async t => { + const opts: GenericOptions = { + kind: 'ERC20', + name: 'é\\")Token', + symbol: 'MTK', + }; + const c = buildERC20(opts); + await runTest(c, t, opts); +}); + async function runTest(c: Contract, t: ExecutionContext<Context>, opts: GenericOptions) { const zip = await zipFoundry(c, opts);
packages/core/solidity/src/zip-foundry.test.ts.md+432 −0 modified@@ -1802,3 +1802,435 @@ Generated by [AVA](https://avajs.dev). }␊ `, ] + +## erc20 name containing double quotes + +> Snapshot 1 + + [ + `#!/usr/bin/env bash␊ + ␊ + # Check if git is installed␊ + if ! which git &> /dev/null␊ + then␊ + echo "git command not found. Install git and try again."␊ + exit 1␊ + fi␊ + ␊ + # Check if Foundry is installed␊ + if ! which forge &> /dev/null␊ + then␊ + echo "forge command not found. Install Foundry and try again. See https://book.getfoundry.sh/getting-started/installation"␊ + exit 1␊ + fi␊ + ␊ + # Setup Foundry project␊ + if ! [ -f "foundry.toml" ]␊ + then␊ + echo "Initializing Foundry project..."␊ + ␊ + # Backup Wizard template readme to avoid it being overwritten␊ + mv README.md README-oz.md␊ + ␊ + # Initialize sample Foundry project␊ + forge init --force --quiet␊ + ␊ + # Install OpenZeppelin Contracts␊ + forge install OpenZeppelin/openzeppelin-contracts@vX.Y.Z --quiet␊ + ␊ + # Remove unneeded Foundry template files␊ + rm src/Counter.sol␊ + rm script/Counter.s.sol␊ + rm test/Counter.t.sol␊ + rm README.md␊ + ␊ + # Restore Wizard template readme␊ + mv README-oz.md README.md␊ + ␊ + # Add remappings␊ + if [ -f "remappings.txt" ]␊ + then␊ + echo "" >> remappings.txt␊ + fi␊ + echo "@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/" >> remappings.txt␊ + ␊ + # Perform initial git commit␊ + git add .␊ + git commit -m "openzeppelin: add wizard output" --quiet␊ + ␊ + echo "Done."␊ + else␊ + echo "Foundry project already initialized."␊ + fi␊ + `, + `# Sample Foundry Project␊ + ␊ + This project demonstrates a basic Foundry use case. It comes with a contract generated by [OpenZeppelin Wizard](https://wizard.openzeppelin.com/), a test for that contract, and a script that deploys that contract.␊ + ␊ + ## Installing Foundry␊ + ␊ + See [Foundry installation guide](https://book.getfoundry.sh/getting-started/installation).␊ + ␊ + ## Initializing the project␊ + ␊ + \`\`\`␊ + bash setup.sh␊ + \`\`\`␊ + ␊ + ## Testing the contract␊ + ␊ + \`\`\`␊ + forge test␊ + \`\`\`␊ + ␊ + ## Deploying the contract␊ + ␊ + You can simulate a deployment by running the script:␊ + ␊ + \`\`\`␊ + forge script script/MyToken.s.sol␊ + \`\`\`␊ + ␊ + See [Solidity scripting guide](https://book.getfoundry.sh/guides/scripting-with-solidity) for more information.␊ + `, + `// SPDX-License-Identifier: MIT␊ + pragma solidity ^0.8.27;␊ + ␊ + import {Script} from "forge-std/Script.sol";␊ + import {console} from "forge-std/console.sol";␊ + import {MyToken} from "src/MyToken.sol";␊ + ␊ + contract MyTokenScript is Script {␊ + function setUp() public {}␊ + ␊ + function run() public {␊ + vm.startBroadcast();␊ + MyToken instance = new MyToken();␊ + console.log("Contract deployed to %s", address(instance));␊ + vm.stopBroadcast();␊ + }␊ + }␊ + `, + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts ^5.6.0␊ + pragma solidity ^0.8.27;␊ + ␊ + import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";␊ + import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";␊ + ␊ + contract MyToken is ERC20, ERC20Permit {␊ + constructor() ERC20("My \\"Token\\"", "MTK") ERC20Permit("My \\"Token\\"") {}␊ + }␊ + `, + `// SPDX-License-Identifier: MIT␊ + pragma solidity ^0.8.27;␊ + ␊ + import {Test} from "forge-std/Test.sol";␊ + import {MyToken} from "src/MyToken.sol";␊ + ␊ + contract MyTokenTest is Test {␊ + MyToken public instance;␊ + ␊ + function setUp() public {␊ + instance = new MyToken();␊ + }␊ + ␊ + function testName() public view {␊ + assertEq(instance.name(), "My \\"Token\\"");␊ + }␊ + }␊ + `, + ] + +## erc1155 uri containing double quotes + +> Snapshot 1 + + [ + `#!/usr/bin/env bash␊ + ␊ + # Check if git is installed␊ + if ! which git &> /dev/null␊ + then␊ + echo "git command not found. Install git and try again."␊ + exit 1␊ + fi␊ + ␊ + # Check if Foundry is installed␊ + if ! which forge &> /dev/null␊ + then␊ + echo "forge command not found. Install Foundry and try again. See https://book.getfoundry.sh/getting-started/installation"␊ + exit 1␊ + fi␊ + ␊ + # Setup Foundry project␊ + if ! [ -f "foundry.toml" ]␊ + then␊ + echo "Initializing Foundry project..."␊ + ␊ + # Backup Wizard template readme to avoid it being overwritten␊ + mv README.md README-oz.md␊ + ␊ + # Initialize sample Foundry project␊ + forge init --force --quiet␊ + ␊ + # Install OpenZeppelin Contracts␊ + forge install OpenZeppelin/openzeppelin-contracts@vX.Y.Z --quiet␊ + ␊ + # Remove unneeded Foundry template files␊ + rm src/Counter.sol␊ + rm script/Counter.s.sol␊ + rm test/Counter.t.sol␊ + rm README.md␊ + ␊ + # Restore Wizard template readme␊ + mv README-oz.md README.md␊ + ␊ + # Add remappings␊ + if [ -f "remappings.txt" ]␊ + then␊ + echo "" >> remappings.txt␊ + fi␊ + echo "@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/" >> remappings.txt␊ + ␊ + # Perform initial git commit␊ + git add .␊ + git commit -m "openzeppelin: add wizard output" --quiet␊ + ␊ + echo "Done."␊ + else␊ + echo "Foundry project already initialized."␊ + fi␊ + `, + `# Sample Foundry Project␊ + ␊ + This project demonstrates a basic Foundry use case. It comes with a contract generated by [OpenZeppelin Wizard](https://wizard.openzeppelin.com/), a test for that contract, and a script that deploys that contract.␊ + ␊ + ## Installing Foundry␊ + ␊ + See [Foundry installation guide](https://book.getfoundry.sh/getting-started/installation).␊ + ␊ + ## Initializing the project␊ + ␊ + \`\`\`␊ + bash setup.sh␊ + \`\`\`␊ + ␊ + ## Testing the contract␊ + ␊ + \`\`\`␊ + forge test␊ + \`\`\`␊ + ␊ + ## Deploying the contract␊ + ␊ + You can simulate a deployment by running the script:␊ + ␊ + \`\`\`␊ + forge script script/MyToken.s.sol␊ + \`\`\`␊ + ␊ + See [Solidity scripting guide](https://book.getfoundry.sh/guides/scripting-with-solidity) for more information.␊ + `, + `// SPDX-License-Identifier: MIT␊ + pragma solidity ^0.8.27;␊ + ␊ + import {Script} from "forge-std/Script.sol";␊ + import {console} from "forge-std/console.sol";␊ + import {MyToken} from "src/MyToken.sol";␊ + ␊ + contract MyTokenScript is Script {␊ + function setUp() public {}␊ + ␊ + function run() public {␊ + // TODO: Set addresses for the variables below, then uncomment the following section:␊ + /*␊ + vm.startBroadcast();␊ + address initialOwner = <Set initialOwner address here>;␊ + MyToken instance = new MyToken(initialOwner);␊ + console.log("Contract deployed to %s", address(instance));␊ + vm.stopBroadcast();␊ + */␊ + }␊ + }␊ + `, + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts ^5.6.0␊ + pragma solidity ^0.8.27;␊ + ␊ + import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";␊ + import {ERC1155} from "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";␊ + ␊ + contract MyToken is ERC1155, Ownable {␊ + constructor(address initialOwner)␊ + ERC1155("https://example.com/\\"id\\"/{id}")␊ + Ownable(initialOwner)␊ + {}␊ + ␊ + function setURI(string memory newuri) public onlyOwner {␊ + _setURI(newuri);␊ + }␊ + }␊ + `, + `// SPDX-License-Identifier: MIT␊ + pragma solidity ^0.8.27;␊ + ␊ + import {Test} from "forge-std/Test.sol";␊ + import {MyToken} from "src/MyToken.sol";␊ + ␊ + contract MyTokenTest is Test {␊ + MyToken public instance;␊ + ␊ + function setUp() public {␊ + address initialOwner = vm.addr(1);␊ + instance = new MyToken(initialOwner);␊ + }␊ + ␊ + function testUri() public view {␊ + assertEq(instance.uri(0), "https://example.com/\\"id\\"/{id}");␊ + }␊ + }␊ + `, + ] + +## erc20 name containing unicode, backslash, and double quotes + +> Snapshot 1 + + [ + `#!/usr/bin/env bash␊ + ␊ + # Check if git is installed␊ + if ! which git &> /dev/null␊ + then␊ + echo "git command not found. Install git and try again."␊ + exit 1␊ + fi␊ + ␊ + # Check if Foundry is installed␊ + if ! which forge &> /dev/null␊ + then␊ + echo "forge command not found. Install Foundry and try again. See https://book.getfoundry.sh/getting-started/installation"␊ + exit 1␊ + fi␊ + ␊ + # Setup Foundry project␊ + if ! [ -f "foundry.toml" ]␊ + then␊ + echo "Initializing Foundry project..."␊ + ␊ + # Backup Wizard template readme to avoid it being overwritten␊ + mv README.md README-oz.md␊ + ␊ + # Initialize sample Foundry project␊ + forge init --force --quiet␊ + ␊ + # Install OpenZeppelin Contracts␊ + forge install OpenZeppelin/openzeppelin-contracts@vX.Y.Z --quiet␊ + ␊ + # Remove unneeded Foundry template files␊ + rm src/Counter.sol␊ + rm script/Counter.s.sol␊ + rm test/Counter.t.sol␊ + rm README.md␊ + ␊ + # Restore Wizard template readme␊ + mv README-oz.md README.md␊ + ␊ + # Add remappings␊ + if [ -f "remappings.txt" ]␊ + then␊ + echo "" >> remappings.txt␊ + fi␊ + echo "@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/" >> remappings.txt␊ + ␊ + # Perform initial git commit␊ + git add .␊ + git commit -m "openzeppelin: add wizard output" --quiet␊ + ␊ + echo "Done."␊ + else␊ + echo "Foundry project already initialized."␊ + fi␊ + `, + `# Sample Foundry Project␊ + ␊ + This project demonstrates a basic Foundry use case. It comes with a contract generated by [OpenZeppelin Wizard](https://wizard.openzeppelin.com/), a test for that contract, and a script that deploys that contract.␊ + ␊ + ## Installing Foundry␊ + ␊ + See [Foundry installation guide](https://book.getfoundry.sh/getting-started/installation).␊ + ␊ + ## Initializing the project␊ + ␊ + \`\`\`␊ + bash setup.sh␊ + \`\`\`␊ + ␊ + ## Testing the contract␊ + ␊ + \`\`\`␊ + forge test␊ + \`\`\`␊ + ␊ + ## Deploying the contract␊ + ␊ + You can simulate a deployment by running the script:␊ + ␊ + \`\`\`␊ + forge script script/EToken.s.sol␊ + \`\`\`␊ + ␊ + See [Solidity scripting guide](https://book.getfoundry.sh/guides/scripting-with-solidity) for more information.␊ + `, + `// SPDX-License-Identifier: MIT␊ + pragma solidity ^0.8.27;␊ + ␊ + import {Script} from "forge-std/Script.sol";␊ + import {console} from "forge-std/console.sol";␊ + import {EToken} from "src/EToken.sol";␊ + ␊ + contract ETokenScript is Script {␊ + function setUp() public {}␊ + ␊ + function run() public {␊ + vm.startBroadcast();␊ + EToken instance = new EToken();␊ + console.log("Contract deployed to %s", address(instance));␊ + vm.stopBroadcast();␊ + }␊ + }␊ + `, + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts ^5.6.0␊ + pragma solidity ^0.8.27;␊ + ␊ + import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";␊ + import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";␊ + ␊ + contract EToken is ERC20, ERC20Permit {␊ + constructor()␊ + ERC20(unicode"é\\\\\\")Token", "MTK")␊ + ERC20Permit(unicode"é\\\\\\")Token")␊ + {}␊ + }␊ + `, + `// SPDX-License-Identifier: MIT␊ + pragma solidity ^0.8.27;␊ + ␊ + import {Test} from "forge-std/Test.sol";␊ + import {EToken} from "src/EToken.sol";␊ + ␊ + contract ETokenTest is Test {␊ + EToken public instance;␊ + ␊ + function setUp() public {␊ + instance = new EToken();␊ + }␊ + ␊ + function testName() public view {␊ + assertEq(instance.name(), unicode"é\\\\\\")Token");␊ + }␊ + }␊ + `, + ]
packages/core/solidity/src/zip-foundry.test.ts.snap+0 −0 modifiedpackages/core/solidity/src/zip-foundry.ts+11 −2 modified@@ -6,6 +6,7 @@ import SOLIDITY_VERSION from './solidity-version.json'; import contracts from '../openzeppelin-contracts'; import type { Lines } from './utils/format-lines'; import { formatLinesWithSpaces, spaceBetween } from './utils/format-lines'; +import { stringifyUnicodeSafe } from './utils/sanitize'; import type { Upgradeable } from './set-upgradeable'; function getHeader(c: Contract) { @@ -118,10 +119,18 @@ const test = (c: Contract, opts?: GenericOptions) => { switch (opts.kind) { case 'ERC20': case 'ERC721': - return ['function testName() public view {', [`assertEq(instance.name(), "${opts.name}");`], '}']; + return [ + 'function testName() public view {', + [`assertEq(instance.name(), ${stringifyUnicodeSafe(opts.name)});`], + '}', + ]; case 'ERC1155': - return ['function testUri() public view {', [`assertEq(instance.uri(0), "${opts.uri}");`], '}']; + return [ + 'function testUri() public view {', + [`assertEq(instance.uri(0), ${stringifyUnicodeSafe(opts.uri)});`], + '}', + ]; case 'Account': case 'Governor':
packages/core/solidity/src/zip-hardhat.test.ts+20 −0 modified@@ -121,6 +121,26 @@ test.serial('custom upgradeable', async t => { await runDeployScriptTest(c, t, opts); }); +test.serial('erc20 name containing double quotes', async t => { + const opts: GenericOptions = { + kind: 'ERC20', + name: 'My "Token"', + symbol: 'MTK', + }; + const c = buildERC20(opts); + await runIgnitionTest(c, t, opts); +}); + +test.serial('erc1155 uri containing double quotes', async t => { + const opts: GenericOptions = { + kind: 'ERC1155', + name: 'My Token', + uri: 'https://example.com/"id"/{id}', + }; + const c = buildERC1155(opts); + await runIgnitionTest(c, t, opts); +}); + async function runDeployScriptTest(c: Contract, t: ExecutionContext<Context>, opts: GenericOptions) { const zip = await zipHardhat(c, opts);
packages/core/solidity/src/zip-hardhat.test.ts.md+161 −0 modified@@ -849,3 +849,164 @@ Generated by [AVA](https://avajs.dev). });␊ `, ] + +## erc20 name containing double quotes + +> Snapshot 1 + + [ + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts ^5.6.0␊ + pragma solidity ^0.8.27;␊ + ␊ + import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";␊ + import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";␊ + ␊ + contract MyToken is ERC20, ERC20Permit {␊ + constructor() ERC20("My \\"Token\\"", "MTK") ERC20Permit("My \\"Token\\"") {}␊ + }␊ + `, + `import { HardhatUserConfig } from "hardhat/config";␊ + import "@nomicfoundation/hardhat-toolbox";␊ + ␊ + ␊ + const config: HardhatUserConfig = {␊ + solidity: {␊ + version: "0.8.27",␊ + settings: {␊ + evmVersion: 'cancun',␊ + optimizer: {␊ + enabled: true,␊ + },␊ + },␊ + },␊ + };␊ + ␊ + export default config;␊ + `, + `{␊ + "name": "hardhat-sample",␊ + "version": "0.0.1",␊ + "description": "",␊ + "main": "index.js",␊ + "scripts": {␊ + "test": "hardhat test"␊ + },␊ + "author": "",␊ + "license": "MIT",␊ + "devDependencies": {␊ + "@nomicfoundation/hardhat-toolbox": "^6.1.0",␊ + "@openzeppelin/contracts": "^5.6.0",␊ + "hardhat": "^2.22.0"␊ + }␊ + }`, + `import { buildModule } from "@nomicfoundation/hardhat-ignition/modules";␊ + ␊ + export default buildModule("MyTokenModule", (m) => {␊ + ␊ + ␊ + const myToken = m.contract("MyToken", []);␊ + ␊ + return { myToken };␊ + });␊ + `, + `import { expect } from "chai";␊ + import { ethers } from "hardhat";␊ + ␊ + describe("MyToken", function () {␊ + it("Test contract", async function () {␊ + const ContractFactory = await ethers.getContractFactory("MyToken");␊ + ␊ + const instance = await ContractFactory.deploy();␊ + await instance.waitForDeployment();␊ + ␊ + expect(await instance.name()).to.equal("My \\"Token\\"");␊ + });␊ + });␊ + `, + ] + +## erc1155 uri containing double quotes + +> Snapshot 1 + + [ + `// SPDX-License-Identifier: MIT␊ + // Compatible with OpenZeppelin Contracts ^5.6.0␊ + pragma solidity ^0.8.27;␊ + ␊ + import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";␊ + import {ERC1155} from "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";␊ + ␊ + contract MyToken is ERC1155, Ownable {␊ + constructor(address initialOwner)␊ + ERC1155("https://example.com/\\"id\\"/{id}")␊ + Ownable(initialOwner)␊ + {}␊ + ␊ + function setURI(string memory newuri) public onlyOwner {␊ + _setURI(newuri);␊ + }␊ + }␊ + `, + `import { HardhatUserConfig } from "hardhat/config";␊ + import "@nomicfoundation/hardhat-toolbox";␊ + ␊ + ␊ + const config: HardhatUserConfig = {␊ + solidity: {␊ + version: "0.8.27",␊ + settings: {␊ + evmVersion: 'cancun',␊ + optimizer: {␊ + enabled: true,␊ + },␊ + },␊ + },␊ + };␊ + ␊ + export default config;␊ + `, + `{␊ + "name": "hardhat-sample",␊ + "version": "0.0.1",␊ + "description": "",␊ + "main": "index.js",␊ + "scripts": {␊ + "test": "hardhat test"␊ + },␊ + "author": "",␊ + "license": "MIT",␊ + "devDependencies": {␊ + "@nomicfoundation/hardhat-toolbox": "^6.1.0",␊ + "@openzeppelin/contracts": "^5.6.0",␊ + "hardhat": "^2.22.0"␊ + }␊ + }`, + `import { buildModule } from "@nomicfoundation/hardhat-ignition/modules";␊ + ␊ + export default buildModule("MyTokenModule", (m) => {␊ + ␊ + // TODO: Set values for the constructor arguments below␊ + const myToken = m.contract("MyToken", [initialOwner]);␊ + ␊ + return { myToken };␊ + });␊ + `, + `import { expect } from "chai";␊ + import { ethers } from "hardhat";␊ + ␊ + describe("MyToken", function () {␊ + it("Test contract", async function () {␊ + const ContractFactory = await ethers.getContractFactory("MyToken");␊ + ␊ + const initialOwner = (await ethers.getSigners())[0].address;␊ + ␊ + const instance = await ContractFactory.deploy(initialOwner);␊ + await instance.waitForDeployment();␊ + ␊ + expect(await instance.uri(0)).to.equal("https://example.com/\\"id\\"/{id}");␊ + });␊ + });␊ + `, + ]
packages/core/solidity/src/zip-hardhat.test.ts.snap+0 −0 modifiedpackages/core/solidity/src/zip-hardhat.ts+2 −2 modified@@ -45,9 +45,9 @@ class TestGenerator { switch (opts.kind) { case 'ERC20': case 'ERC721': - return [`expect(await instance.name()).to.equal("${opts.name}");`]; + return [`expect(await instance.name()).to.equal(${JSON.stringify(opts.name)});`]; case 'ERC1155': - return [`expect(await instance.uri(0)).to.equal("${opts.uri}");`]; + return [`expect(await instance.uri(0)).to.equal(${JSON.stringify(opts.uri)});`]; case 'Account': case 'Governor': case 'Custom':
Vulnerability mechanics
Root cause
"Missing escaping of user-supplied `opts.name` and `opts.uri` strings when interpolating them into generated Hardhat and Foundry test file source code."
Attack vector
An attacker crafts a malicious `opts.name` or `opts.uri` string containing unescaped double quotes, backslashes, or control characters. When the OpenZeppelin Contracts Wizard generates a downloadable project archive via `zipHardhat` or `zipFoundry`, the malicious string is interpolated directly into the test file source code. If a developer downloads and runs `npm test` or `forge test` on the generated project, the injected payload breaks out of its string literal and is parsed as executable code [ref_id=2][ref_id=3]. The attack requires no authentication and is triggered simply by the developer running the test suite.
Affected code
The vulnerability resides in the `zipHardhat` and `zipFoundry` functions in `packages/core/solidity/src/zip-hardhat.ts` and `packages/core/solidity/src/zip-foundry.ts`. These functions interpolated user-supplied `opts.name` and `opts.uri` values directly into generated Hardhat (`test/test.ts`) and Foundry (`test/<Name>.t.sol`) test files without escaping special characters. The fix modifies the `stringifyUnicodeSafe` utility in `packages/core/solidity/src/utils/sanitize.ts` to properly escape backslashes, double quotes, and other control characters according to the Solidity grammar [patch_id=5594805].
What the fix does
The patch rewrites the `stringifyUnicodeSafe` function in `packages/core/solidity/src/utils/sanitize.ts` to properly escape characters according to the Solidity grammar [patch_id=5594805]. For ASCII-only strings, backslashes and double quotes are now escaped with a backslash prefix. For strings containing Unicode characters, the function additionally escapes carriage returns, line feeds, and vertical tabs (0x0B) which the Solidity lexer treats as line terminators. The previous implementation only escaped double quotes in the Unicode branch and used `JSON.stringify` for ASCII strings, which did not handle backslashes or control characters correctly. New test cases in `zip-foundry.test.ts` and `zip-hardhat.test.ts` verify that double quotes, backslashes, and mixed Unicode/special characters are safely escaped in generated test files [ref_id=1].
Preconditions
- inputThe attacker must supply a crafted `opts.name` or `opts.uri` string to a caller of `zipHardhat` or `zipFoundry`.
- authA developer must download the generated project archive and run `npm test` or `forge test`.
Generated on Jun 11, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4- github.com/advisories/GHSA-4x76-22x2-rx8vghsaADVISORY
- github.com/OpenZeppelin/contracts-wizard/commit/ec12c44f8d9e0491eba31037f95b36e98ec58b5fghsa
- github.com/OpenZeppelin/contracts-wizard/releases/tag/%40openzeppelin%2Fwizard%400.10.9ghsa
- github.com/OpenZeppelin/contracts-wizard/security/advisories/GHSA-4x76-22x2-rx8vghsa
News mentions
0No linked articles in our index yet.