diff --git a/packages/core/cairo/CHANGELOG.md b/packages/core/cairo/CHANGELOG.md
index 08aec99ee..c3c6560dc 100644
--- a/packages/core/cairo/CHANGELOG.md
+++ b/packages/core/cairo/CHANGELOG.md
@@ -1,6 +1,10 @@
# Changelog
+## 3.0.1 (2026-06-18)
+
+- Reject line terminators in `info.securityContact` and `info.license` to prevent breaking out of the generated comment lines. Fixes [GHSA-9wxg-vf3r-56hc](https://github.com/OpenZeppelin/contracts-wizard/security/advisories/GHSA-9wxg-vf3r-56hc). ([#818](https://github.com/OpenZeppelin/contracts-wizard/pull/818))
+
## 3.0.0 (2026-01-10)
- Add support for `with_components` macro. ([#703](https://github.com/OpenZeppelin/contracts-wizard/pull/703))
diff --git a/packages/core/cairo/package.json b/packages/core/cairo/package.json
index d1ea7a3a5..450d23617 100644
--- a/packages/core/cairo/package.json
+++ b/packages/core/cairo/package.json
@@ -1,6 +1,6 @@
{
"name": "@openzeppelin/wizard-cairo",
- "version": "3.0.0",
+ "version": "3.0.1",
"description": "A boilerplate generator to get started with OpenZeppelin Contracts for Cairo",
"license": "AGPL-3.0-only",
"repository": "https://github.com/OpenZeppelin/contracts-wizard",
diff --git a/packages/core/cairo/src/set-info.test.ts b/packages/core/cairo/src/set-info.test.ts
new file mode 100644
index 000000000..36deeb1a4
--- /dev/null
+++ b/packages/core/cairo/src/set-info.test.ts
@@ -0,0 +1,33 @@
+import test from 'ava';
+
+import { ContractBuilder } from './contract';
+import { defaults as macrosDefaults } from './set-macros';
+import { setInfo } from './set-info';
+import type { OptionsError } from './error';
+
+const lineBreaks: [string, string][] = [
+ ['LF', '\n'],
+ ['CR', '\r'],
+ ['CRLF', '\r\n'],
+ ['LS', '\u2028'],
+ ['PS', '\u2029'],
+];
+
+for (const [name, ch] of lineBreaks) {
+ test(`setInfo rejects ${name} in securityContact`, t => {
+ const c = new ContractBuilder('MyContract', macrosDefaults);
+ const error = t.throws(() => setInfo(c, { securityContact: `security@example.com${ch}mod injected {}` }));
+ t.is((error as OptionsError).messages.securityContact, 'Must not contain line breaks');
+ });
+
+ test(`setInfo rejects ${name} in license`, t => {
+ const c = new ContractBuilder('MyContract', macrosDefaults);
+ const error = t.throws(() => setInfo(c, { license: `MIT${ch}mod injected {}` }));
+ t.is((error as OptionsError).messages.license, 'Must not contain line breaks');
+ });
+}
+
+test('setInfo accepts valid single-line values', t => {
+ const c = new ContractBuilder('MyContract', macrosDefaults);
+ t.notThrows(() => setInfo(c, { securityContact: 'security@example.com', license: 'MIT' }));
+});
diff --git a/packages/core/cairo/src/set-info.ts b/packages/core/cairo/src/set-info.ts
index fb161d0cd..463a9ae54 100644
--- a/packages/core/cairo/src/set-info.ts
+++ b/packages/core/cairo/src/set-info.ts
@@ -1,7 +1,20 @@
import type { ContractBuilder } from './contract';
+import { OptionsError } from './error';
export const SECURITY_CONTACT_DOCUMENTATION = `Security contact: `;
+// These values are printed into a // comment line in the generated Cairo. LF
+// ends that comment, letting following text become source. CR and U+2028/U+2029
+// are rejected as defense in depth (review tools render U+2028/U+2029 as line
+// breaks).
+const LINE_TERMINATOR = /[\n\r\u2028\u2029]/u;
+
+function checkSingleLine(value: string, field: string): void {
+ if (LINE_TERMINATOR.test(value)) {
+ throw new OptionsError({ [field]: 'Must not contain line breaks' });
+ }
+}
+
export const infoOptions = [{}, { license: 'WTFPL' }] as const;
export const defaults: Info = { license: 'MIT' };
@@ -15,10 +28,12 @@ export function setInfo(c: ContractBuilder, info: Info): void {
const { securityContact, license } = info;
if (securityContact) {
+ checkSingleLine(securityContact, 'securityContact');
c.addDocumentation(`${SECURITY_CONTACT_DOCUMENTATION}${securityContact}`);
}
if (license) {
+ checkSingleLine(license, 'license');
c.license = license;
}
}
diff --git a/packages/core/cairo_alpha/src/set-info.test.ts b/packages/core/cairo_alpha/src/set-info.test.ts
new file mode 100644
index 000000000..36deeb1a4
--- /dev/null
+++ b/packages/core/cairo_alpha/src/set-info.test.ts
@@ -0,0 +1,33 @@
+import test from 'ava';
+
+import { ContractBuilder } from './contract';
+import { defaults as macrosDefaults } from './set-macros';
+import { setInfo } from './set-info';
+import type { OptionsError } from './error';
+
+const lineBreaks: [string, string][] = [
+ ['LF', '\n'],
+ ['CR', '\r'],
+ ['CRLF', '\r\n'],
+ ['LS', '\u2028'],
+ ['PS', '\u2029'],
+];
+
+for (const [name, ch] of lineBreaks) {
+ test(`setInfo rejects ${name} in securityContact`, t => {
+ const c = new ContractBuilder('MyContract', macrosDefaults);
+ const error = t.throws(() => setInfo(c, { securityContact: `security@example.com${ch}mod injected {}` }));
+ t.is((error as OptionsError).messages.securityContact, 'Must not contain line breaks');
+ });
+
+ test(`setInfo rejects ${name} in license`, t => {
+ const c = new ContractBuilder('MyContract', macrosDefaults);
+ const error = t.throws(() => setInfo(c, { license: `MIT${ch}mod injected {}` }));
+ t.is((error as OptionsError).messages.license, 'Must not contain line breaks');
+ });
+}
+
+test('setInfo accepts valid single-line values', t => {
+ const c = new ContractBuilder('MyContract', macrosDefaults);
+ t.notThrows(() => setInfo(c, { securityContact: 'security@example.com', license: 'MIT' }));
+});
diff --git a/packages/core/cairo_alpha/src/set-info.ts b/packages/core/cairo_alpha/src/set-info.ts
index fb161d0cd..463a9ae54 100644
--- a/packages/core/cairo_alpha/src/set-info.ts
+++ b/packages/core/cairo_alpha/src/set-info.ts
@@ -1,7 +1,20 @@
import type { ContractBuilder } from './contract';
+import { OptionsError } from './error';
export const SECURITY_CONTACT_DOCUMENTATION = `Security contact: `;
+// These values are printed into a // comment line in the generated Cairo. LF
+// ends that comment, letting following text become source. CR and U+2028/U+2029
+// are rejected as defense in depth (review tools render U+2028/U+2029 as line
+// breaks).
+const LINE_TERMINATOR = /[\n\r\u2028\u2029]/u;
+
+function checkSingleLine(value: string, field: string): void {
+ if (LINE_TERMINATOR.test(value)) {
+ throw new OptionsError({ [field]: 'Must not contain line breaks' });
+ }
+}
+
export const infoOptions = [{}, { license: 'WTFPL' }] as const;
export const defaults: Info = { license: 'MIT' };
@@ -15,10 +28,12 @@ export function setInfo(c: ContractBuilder, info: Info): void {
const { securityContact, license } = info;
if (securityContact) {
+ checkSingleLine(securityContact, 'securityContact');
c.addDocumentation(`${SECURITY_CONTACT_DOCUMENTATION}${securityContact}`);
}
if (license) {
+ checkSingleLine(license, 'license');
c.license = license;
}
}
diff --git a/packages/core/solidity/CHANGELOG.md b/packages/core/solidity/CHANGELOG.md
index 6b82a3ca0..9e5136e93 100644
--- a/packages/core/solidity/CHANGELOG.md
+++ b/packages/core/solidity/CHANGELOG.md
@@ -1,6 +1,10 @@
# Changelog
+## 0.10.11 (2026-06-18)
+
+- Reject line terminators in `info.securityContact` and `info.license` to prevent breaking out of the generated comment lines. Fixes [GHSA-9wxg-vf3r-56hc](https://github.com/OpenZeppelin/contracts-wizard/security/advisories/GHSA-9wxg-vf3r-56hc). ([#818](https://github.com/OpenZeppelin/contracts-wizard/pull/818))
+
## 0.10.10 (2026-06-11)
- Add optional `decimals` to `erc20`, `stablecoin`, and `realWorldAsset`, which overrides `decimals()` when set to a non-default value. Defaults to 18 as before. ([#812](https://github.com/OpenZeppelin/contracts-wizard/pull/812))
diff --git a/packages/core/solidity/package.json b/packages/core/solidity/package.json
index 0e9c08ef9..1655d4ad9 100644
--- a/packages/core/solidity/package.json
+++ b/packages/core/solidity/package.json
@@ -1,6 +1,6 @@
{
"name": "@openzeppelin/wizard",
- "version": "0.10.10",
+ "version": "0.10.11",
"description": "A boilerplate generator to get started with OpenZeppelin Contracts",
"license": "AGPL-3.0-only",
"repository": "https://github.com/OpenZeppelin/contracts-wizard",
diff --git a/packages/core/solidity/src/set-info.test.ts b/packages/core/solidity/src/set-info.test.ts
new file mode 100644
index 000000000..cc26f9df2
--- /dev/null
+++ b/packages/core/solidity/src/set-info.test.ts
@@ -0,0 +1,32 @@
+import test from 'ava';
+
+import { ContractBuilder } from './contract';
+import { setInfo } from './set-info';
+import type { OptionsError } from './error';
+
+const lineBreaks: [string, string][] = [
+ ['LF', '\n'],
+ ['CR', '\r'],
+ ['CRLF', '\r\n'],
+ ['LS', '\u2028'],
+ ['PS', '\u2029'],
+];
+
+for (const [name, ch] of lineBreaks) {
+ test(`setInfo rejects ${name} in securityContact`, t => {
+ const c = new ContractBuilder('MyContract');
+ const error = t.throws(() => setInfo(c, { securityContact: `security@example.com${ch}contract Injected {}` }));
+ t.is((error as OptionsError).messages.securityContact, 'Must not contain line breaks');
+ });
+
+ test(`setInfo rejects ${name} in license`, t => {
+ const c = new ContractBuilder('MyContract');
+ const error = t.throws(() => setInfo(c, { license: `MIT${ch}contract Injected {}` }));
+ t.is((error as OptionsError).messages.license, 'Must not contain line breaks');
+ });
+}
+
+test('setInfo accepts valid single-line values', t => {
+ const c = new ContractBuilder('MyContract');
+ t.notThrows(() => setInfo(c, { securityContact: 'security@example.com', license: 'MIT' }));
+});
diff --git a/packages/core/solidity/src/set-info.ts b/packages/core/solidity/src/set-info.ts
index 61e3713f4..ed12dd4c2 100644
--- a/packages/core/solidity/src/set-info.ts
+++ b/packages/core/solidity/src/set-info.ts
@@ -1,7 +1,20 @@
import type { ContractBuilder } from './contract';
+import { OptionsError } from './error';
export const TAG_SECURITY_CONTACT = `@custom:security-contact`;
+// LF and CR end a line comment in solc, letting following text escape the
+// generated comment line and become source. U+2028/U+2029 are also rejected:
+// solc errors on them, and review tools render them as line breaks. Other
+// control chars (VT/FF/NEL) error in solc too but cannot silently inject.
+const LINE_TERMINATOR = /[\n\r\u2028\u2029]/u;
+
+function checkSingleLine(value: string, field: string): void {
+ if (LINE_TERMINATOR.test(value)) {
+ throw new OptionsError({ [field]: 'Must not contain line breaks' });
+ }
+}
+
export const infoOptions = [{}, { securityContact: 'security@example.com', license: 'WTFPL' }] as const;
export const defaults: Info = { license: 'MIT' };
@@ -15,10 +28,12 @@ export function setInfo(c: ContractBuilder, info: Info) {
const { securityContact, license } = info;
if (securityContact) {
+ checkSingleLine(securityContact, 'securityContact');
c.addNatspecTag(TAG_SECURITY_CONTACT, securityContact);
}
if (license) {
+ checkSingleLine(license, 'license');
c.license = license;
}
}
diff --git a/packages/core/stellar/CHANGELOG.md b/packages/core/stellar/CHANGELOG.md
index b88a6a2fc..9b1b17d1c 100644
--- a/packages/core/stellar/CHANGELOG.md
+++ b/packages/core/stellar/CHANGELOG.md
@@ -1,6 +1,10 @@
# Changelog
+## 0.6.2 (2026-06-18)
+
+- Reject line terminators in `info.securityContact` and `info.license` to prevent breaking out of the generated comment lines. Fixes [GHSA-9wxg-vf3r-56hc](https://github.com/OpenZeppelin/contracts-wizard/security/advisories/GHSA-9wxg-vf3r-56hc). ([#818](https://github.com/OpenZeppelin/contracts-wizard/pull/818))
+
## 0.6.1 (2026-06-11)
- Update Stellar Contracts to v0.7.2 and Soroban SDK to v26.1.0. ([#815](https://github.com/OpenZeppelin/contracts-wizard/pull/815))
diff --git a/packages/core/stellar/package.json b/packages/core/stellar/package.json
index 557f6cfc1..83e31fbf3 100644
--- a/packages/core/stellar/package.json
+++ b/packages/core/stellar/package.json
@@ -1,6 +1,6 @@
{
"name": "@openzeppelin/wizard-stellar",
- "version": "0.6.1",
+ "version": "0.6.2",
"description": "A boilerplate generator to get started with OpenZeppelin Stellar Soroban Contracts",
"license": "AGPL-3.0-only",
"repository": "https://github.com/OpenZeppelin/contracts-wizard",
diff --git a/packages/core/stellar/src/set-info.test.ts b/packages/core/stellar/src/set-info.test.ts
new file mode 100644
index 000000000..cc26f9df2
--- /dev/null
+++ b/packages/core/stellar/src/set-info.test.ts
@@ -0,0 +1,32 @@
+import test from 'ava';
+
+import { ContractBuilder } from './contract';
+import { setInfo } from './set-info';
+import type { OptionsError } from './error';
+
+const lineBreaks: [string, string][] = [
+ ['LF', '\n'],
+ ['CR', '\r'],
+ ['CRLF', '\r\n'],
+ ['LS', '\u2028'],
+ ['PS', '\u2029'],
+];
+
+for (const [name, ch] of lineBreaks) {
+ test(`setInfo rejects ${name} in securityContact`, t => {
+ const c = new ContractBuilder('MyContract');
+ const error = t.throws(() => setInfo(c, { securityContact: `security@example.com${ch}contract Injected {}` }));
+ t.is((error as OptionsError).messages.securityContact, 'Must not contain line breaks');
+ });
+
+ test(`setInfo rejects ${name} in license`, t => {
+ const c = new ContractBuilder('MyContract');
+ const error = t.throws(() => setInfo(c, { license: `MIT${ch}contract Injected {}` }));
+ t.is((error as OptionsError).messages.license, 'Must not contain line breaks');
+ });
+}
+
+test('setInfo accepts valid single-line values', t => {
+ const c = new ContractBuilder('MyContract');
+ t.notThrows(() => setInfo(c, { securityContact: 'security@example.com', license: 'MIT' }));
+});
diff --git a/packages/core/stellar/src/set-info.ts b/packages/core/stellar/src/set-info.ts
index 9a71f7def..1796f2f8b 100644
--- a/packages/core/stellar/src/set-info.ts
+++ b/packages/core/stellar/src/set-info.ts
@@ -1,4 +1,17 @@
import type { ContractBuilder } from './contract';
+import { OptionsError } from './error';
+
+// These values are printed into a // comment line in the generated Rust. LF
+// ends that comment, letting following text become source. CR and U+2028/U+2029
+// cannot break out in Rust, but are rejected as defense in depth (review tools
+// render U+2028/U+2029 as line breaks).
+const LINE_TERMINATOR = /[\n\r\u2028\u2029]/u;
+
+function checkSingleLine(value: string, field: string): void {
+ if (LINE_TERMINATOR.test(value)) {
+ throw new OptionsError({ [field]: 'Must not contain line breaks' });
+ }
+}
export const infoOptions = [{}, { license: 'WTFPL' }] as const;
@@ -10,7 +23,13 @@ export type Info = {
};
export function setInfo(c: ContractBuilder, { securityContact, license }: Info): void {
- if (securityContact) c.addContractMetadata({ key: 'security_contact', value: securityContact });
+ if (securityContact) {
+ checkSingleLine(securityContact, 'securityContact');
+ c.addContractMetadata({ key: 'security_contact', value: securityContact });
+ }
- if (license) c.license = license;
+ if (license) {
+ checkSingleLine(license, 'license');
+ c.license = license;
+ }
}
diff --git a/packages/core/stylus/CHANGELOG.md b/packages/core/stylus/CHANGELOG.md
index 2a1f651dd..6290db2c4 100644
--- a/packages/core/stylus/CHANGELOG.md
+++ b/packages/core/stylus/CHANGELOG.md
@@ -1,6 +1,10 @@
# Changelog
+## 0.3.1 (2026-06-18)
+
+- Reject line terminators in `info.securityContact` and `info.license` to prevent breaking out of the generated comment lines. Fixes [GHSA-9wxg-vf3r-56hc](https://github.com/OpenZeppelin/contracts-wizard/security/advisories/GHSA-9wxg-vf3r-56hc). ([#818](https://github.com/OpenZeppelin/contracts-wizard/pull/818))
+
## 0.3.0 (2025-12-12)
- Refactor Stylus templates to match the official v0.3.0 release ([#753](https://github.com/OpenZeppelin/contracts-wizard/pull/753))
diff --git a/packages/core/stylus/package.json b/packages/core/stylus/package.json
index 8835a6a7b..6469fd210 100644
--- a/packages/core/stylus/package.json
+++ b/packages/core/stylus/package.json
@@ -1,6 +1,6 @@
{
"name": "@openzeppelin/wizard-stylus",
- "version": "0.3.0",
+ "version": "0.3.1",
"description": "A boilerplate generator to get started with OpenZeppelin Contracts for Stylus",
"license": "AGPL-3.0-only",
"repository": "https://github.com/OpenZeppelin/contracts-wizard",
diff --git a/packages/core/stylus/src/set-info.test.ts b/packages/core/stylus/src/set-info.test.ts
new file mode 100644
index 000000000..cc26f9df2
--- /dev/null
+++ b/packages/core/stylus/src/set-info.test.ts
@@ -0,0 +1,32 @@
+import test from 'ava';
+
+import { ContractBuilder } from './contract';
+import { setInfo } from './set-info';
+import type { OptionsError } from './error';
+
+const lineBreaks: [string, string][] = [
+ ['LF', '\n'],
+ ['CR', '\r'],
+ ['CRLF', '\r\n'],
+ ['LS', '\u2028'],
+ ['PS', '\u2029'],
+];
+
+for (const [name, ch] of lineBreaks) {
+ test(`setInfo rejects ${name} in securityContact`, t => {
+ const c = new ContractBuilder('MyContract');
+ const error = t.throws(() => setInfo(c, { securityContact: `security@example.com${ch}contract Injected {}` }));
+ t.is((error as OptionsError).messages.securityContact, 'Must not contain line breaks');
+ });
+
+ test(`setInfo rejects ${name} in license`, t => {
+ const c = new ContractBuilder('MyContract');
+ const error = t.throws(() => setInfo(c, { license: `MIT${ch}contract Injected {}` }));
+ t.is((error as OptionsError).messages.license, 'Must not contain line breaks');
+ });
+}
+
+test('setInfo accepts valid single-line values', t => {
+ const c = new ContractBuilder('MyContract');
+ t.notThrows(() => setInfo(c, { securityContact: 'security@example.com', license: 'MIT' }));
+});
diff --git a/packages/core/stylus/src/set-info.ts b/packages/core/stylus/src/set-info.ts
index ebc916923..0f97de53f 100644
--- a/packages/core/stylus/src/set-info.ts
+++ b/packages/core/stylus/src/set-info.ts
@@ -1,4 +1,17 @@
import type { ContractBuilder } from './contract';
+import { OptionsError } from './error';
+
+// These values are printed into a // comment line in the generated Rust. LF
+// ends that comment, letting following text become source. CR and U+2028/U+2029
+// cannot break out in Rust, but are rejected as defense in depth (review tools
+// render U+2028/U+2029 as line breaks).
+const LINE_TERMINATOR = /[\n\r\u2028\u2029]/u;
+
+function checkSingleLine(value: string, field: string): void {
+ if (LINE_TERMINATOR.test(value)) {
+ throw new OptionsError({ [field]: 'Must not contain line breaks' });
+ }
+}
export const infoOptions = [{}, { license: 'WTFPL' }] as const;
@@ -13,10 +26,12 @@ export function setInfo(c: ContractBuilder, info: Info): void {
const { securityContact, license } = info;
if (securityContact) {
+ checkSingleLine(securityContact, 'securityContact');
c.addSecurityTag(securityContact);
}
if (license) {
+ checkSingleLine(license, 'license');
c.license = license;
}
}
diff --git a/packages/ui/src/cairo/AccountControls.svelte b/packages/ui/src/cairo/AccountControls.svelte
index ba1b50138..469dd11d6 100644
--- a/packages/ui/src/cairo/AccountControls.svelte
+++ b/packages/ui/src/cairo/AccountControls.svelte
@@ -87,4 +87,4 @@