Skip to content

Commit 6b18ecb

Browse files
ericglauclaude
andauthored
Reject line terminators in info.securityContact and info.license (#818)
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 4f50cc9 commit 6b18ecb

51 files changed

Lines changed: 323 additions & 55 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changeset/quick-spiders-cheer.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@openzeppelin/wizard': patch
3+
'@openzeppelin/wizard-cairo': patch
4+
'@openzeppelin/wizard-stellar': patch
5+
'@openzeppelin/wizard-stylus': patch
6+
---
7+
8+
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).
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import test from 'ava';
2+
3+
import { ContractBuilder } from './contract';
4+
import { defaults as macrosDefaults } from './set-macros';
5+
import { setInfo } from './set-info';
6+
import type { OptionsError } from './error';
7+
8+
const lineBreaks: [string, string][] = [
9+
['LF', '\n'],
10+
['CR', '\r'],
11+
['CRLF', '\r\n'],
12+
['LS', '\u2028'],
13+
['PS', '\u2029'],
14+
];
15+
16+
for (const [name, ch] of lineBreaks) {
17+
test(`setInfo rejects ${name} in securityContact`, t => {
18+
const c = new ContractBuilder('MyContract', macrosDefaults);
19+
const error = t.throws(() => setInfo(c, { securityContact: `security@example.com${ch}mod injected {}` }));
20+
t.is((error as OptionsError).messages.securityContact, 'Must not contain line breaks');
21+
});
22+
23+
test(`setInfo rejects ${name} in license`, t => {
24+
const c = new ContractBuilder('MyContract', macrosDefaults);
25+
const error = t.throws(() => setInfo(c, { license: `MIT${ch}mod injected {}` }));
26+
t.is((error as OptionsError).messages.license, 'Must not contain line breaks');
27+
});
28+
}
29+
30+
test('setInfo accepts valid single-line values', t => {
31+
const c = new ContractBuilder('MyContract', macrosDefaults);
32+
t.notThrows(() => setInfo(c, { securityContact: 'security@example.com', license: 'MIT' }));
33+
});

packages/core/cairo/src/set-info.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,20 @@
11
import type { ContractBuilder } from './contract';
2+
import { OptionsError } from './error';
23

34
export const SECURITY_CONTACT_DOCUMENTATION = `Security contact: `;
45

6+
// These values are printed into a // comment line in the generated Cairo. LF
7+
// ends that comment, letting following text become source. CR and U+2028/U+2029
8+
// are rejected as defense in depth (review tools render U+2028/U+2029 as line
9+
// breaks).
10+
const LINE_TERMINATOR = /[\n\r\u2028\u2029]/u;
11+
12+
function checkSingleLine(value: string, field: string): void {
13+
if (LINE_TERMINATOR.test(value)) {
14+
throw new OptionsError({ [field]: 'Must not contain line breaks' });
15+
}
16+
}
17+
518
export const infoOptions = [{}, { license: 'WTFPL' }] as const;
619

720
export const defaults: Info = { license: 'MIT' };
@@ -15,10 +28,12 @@ export function setInfo(c: ContractBuilder, info: Info): void {
1528
const { securityContact, license } = info;
1629

1730
if (securityContact) {
31+
checkSingleLine(securityContact, 'securityContact');
1832
c.addDocumentation(`${SECURITY_CONTACT_DOCUMENTATION}${securityContact}`);
1933
}
2034

2135
if (license) {
36+
checkSingleLine(license, 'license');
2237
c.license = license;
2338
}
2439
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import test from 'ava';
2+
3+
import { ContractBuilder } from './contract';
4+
import { defaults as macrosDefaults } from './set-macros';
5+
import { setInfo } from './set-info';
6+
import type { OptionsError } from './error';
7+
8+
const lineBreaks: [string, string][] = [
9+
['LF', '\n'],
10+
['CR', '\r'],
11+
['CRLF', '\r\n'],
12+
['LS', '\u2028'],
13+
['PS', '\u2029'],
14+
];
15+
16+
for (const [name, ch] of lineBreaks) {
17+
test(`setInfo rejects ${name} in securityContact`, t => {
18+
const c = new ContractBuilder('MyContract', macrosDefaults);
19+
const error = t.throws(() => setInfo(c, { securityContact: `security@example.com${ch}mod injected {}` }));
20+
t.is((error as OptionsError).messages.securityContact, 'Must not contain line breaks');
21+
});
22+
23+
test(`setInfo rejects ${name} in license`, t => {
24+
const c = new ContractBuilder('MyContract', macrosDefaults);
25+
const error = t.throws(() => setInfo(c, { license: `MIT${ch}mod injected {}` }));
26+
t.is((error as OptionsError).messages.license, 'Must not contain line breaks');
27+
});
28+
}
29+
30+
test('setInfo accepts valid single-line values', t => {
31+
const c = new ContractBuilder('MyContract', macrosDefaults);
32+
t.notThrows(() => setInfo(c, { securityContact: 'security@example.com', license: 'MIT' }));
33+
});

packages/core/cairo_alpha/src/set-info.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,20 @@
11
import type { ContractBuilder } from './contract';
2+
import { OptionsError } from './error';
23

34
export const SECURITY_CONTACT_DOCUMENTATION = `Security contact: `;
45

6+
// These values are printed into a // comment line in the generated Cairo. LF
7+
// ends that comment, letting following text become source. CR and U+2028/U+2029
8+
// are rejected as defense in depth (review tools render U+2028/U+2029 as line
9+
// breaks).
10+
const LINE_TERMINATOR = /[\n\r\u2028\u2029]/u;
11+
12+
function checkSingleLine(value: string, field: string): void {
13+
if (LINE_TERMINATOR.test(value)) {
14+
throw new OptionsError({ [field]: 'Must not contain line breaks' });
15+
}
16+
}
17+
518
export const infoOptions = [{}, { license: 'WTFPL' }] as const;
619

720
export const defaults: Info = { license: 'MIT' };
@@ -15,10 +28,12 @@ export function setInfo(c: ContractBuilder, info: Info): void {
1528
const { securityContact, license } = info;
1629

1730
if (securityContact) {
31+
checkSingleLine(securityContact, 'securityContact');
1832
c.addDocumentation(`${SECURITY_CONTACT_DOCUMENTATION}${securityContact}`);
1933
}
2034

2135
if (license) {
36+
checkSingleLine(license, 'license');
2237
c.license = license;
2338
}
2439
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import test from 'ava';
2+
3+
import { ContractBuilder } from './contract';
4+
import { setInfo } from './set-info';
5+
import type { OptionsError } from './error';
6+
7+
const lineBreaks: [string, string][] = [
8+
['LF', '\n'],
9+
['CR', '\r'],
10+
['CRLF', '\r\n'],
11+
['LS', '\u2028'],
12+
['PS', '\u2029'],
13+
];
14+
15+
for (const [name, ch] of lineBreaks) {
16+
test(`setInfo rejects ${name} in securityContact`, t => {
17+
const c = new ContractBuilder('MyContract');
18+
const error = t.throws(() => setInfo(c, { securityContact: `security@example.com${ch}contract Injected {}` }));
19+
t.is((error as OptionsError).messages.securityContact, 'Must not contain line breaks');
20+
});
21+
22+
test(`setInfo rejects ${name} in license`, t => {
23+
const c = new ContractBuilder('MyContract');
24+
const error = t.throws(() => setInfo(c, { license: `MIT${ch}contract Injected {}` }));
25+
t.is((error as OptionsError).messages.license, 'Must not contain line breaks');
26+
});
27+
}
28+
29+
test('setInfo accepts valid single-line values', t => {
30+
const c = new ContractBuilder('MyContract');
31+
t.notThrows(() => setInfo(c, { securityContact: 'security@example.com', license: 'MIT' }));
32+
});

packages/core/solidity/src/set-info.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,20 @@
11
import type { ContractBuilder } from './contract';
2+
import { OptionsError } from './error';
23

34
export const TAG_SECURITY_CONTACT = `@custom:security-contact`;
45

6+
// LF and CR end a line comment in solc, letting following text escape the
7+
// generated comment line and become source. U+2028/U+2029 are also rejected:
8+
// solc errors on them, and review tools render them as line breaks. Other
9+
// control chars (VT/FF/NEL) error in solc too but cannot silently inject.
10+
const LINE_TERMINATOR = /[\n\r\u2028\u2029]/u;
11+
12+
function checkSingleLine(value: string, field: string): void {
13+
if (LINE_TERMINATOR.test(value)) {
14+
throw new OptionsError({ [field]: 'Must not contain line breaks' });
15+
}
16+
}
17+
518
export const infoOptions = [{}, { securityContact: 'security@example.com', license: 'WTFPL' }] as const;
619

720
export const defaults: Info = { license: 'MIT' };
@@ -15,10 +28,12 @@ export function setInfo(c: ContractBuilder, info: Info) {
1528
const { securityContact, license } = info;
1629

1730
if (securityContact) {
31+
checkSingleLine(securityContact, 'securityContact');
1832
c.addNatspecTag(TAG_SECURITY_CONTACT, securityContact);
1933
}
2034

2135
if (license) {
36+
checkSingleLine(license, 'license');
2237
c.license = license;
2338
}
2439
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import test from 'ava';
2+
3+
import { ContractBuilder } from './contract';
4+
import { setInfo } from './set-info';
5+
import type { OptionsError } from './error';
6+
7+
const lineBreaks: [string, string][] = [
8+
['LF', '\n'],
9+
['CR', '\r'],
10+
['CRLF', '\r\n'],
11+
['LS', '\u2028'],
12+
['PS', '\u2029'],
13+
];
14+
15+
for (const [name, ch] of lineBreaks) {
16+
test(`setInfo rejects ${name} in securityContact`, t => {
17+
const c = new ContractBuilder('MyContract');
18+
const error = t.throws(() => setInfo(c, { securityContact: `security@example.com${ch}contract Injected {}` }));
19+
t.is((error as OptionsError).messages.securityContact, 'Must not contain line breaks');
20+
});
21+
22+
test(`setInfo rejects ${name} in license`, t => {
23+
const c = new ContractBuilder('MyContract');
24+
const error = t.throws(() => setInfo(c, { license: `MIT${ch}contract Injected {}` }));
25+
t.is((error as OptionsError).messages.license, 'Must not contain line breaks');
26+
});
27+
}
28+
29+
test('setInfo accepts valid single-line values', t => {
30+
const c = new ContractBuilder('MyContract');
31+
t.notThrows(() => setInfo(c, { securityContact: 'security@example.com', license: 'MIT' }));
32+
});

packages/core/stellar/src/set-info.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,17 @@
11
import type { ContractBuilder } from './contract';
2+
import { OptionsError } from './error';
3+
4+
// These values are printed into a // comment line in the generated Rust. LF
5+
// ends that comment, letting following text become source. CR and U+2028/U+2029
6+
// cannot break out in Rust, but are rejected as defense in depth (review tools
7+
// render U+2028/U+2029 as line breaks).
8+
const LINE_TERMINATOR = /[\n\r\u2028\u2029]/u;
9+
10+
function checkSingleLine(value: string, field: string): void {
11+
if (LINE_TERMINATOR.test(value)) {
12+
throw new OptionsError({ [field]: 'Must not contain line breaks' });
13+
}
14+
}
215

316
export const infoOptions = [{}, { license: 'WTFPL' }] as const;
417

@@ -10,7 +23,13 @@ export type Info = {
1023
};
1124

1225
export function setInfo(c: ContractBuilder, { securityContact, license }: Info): void {
13-
if (securityContact) c.addContractMetadata({ key: 'security_contact', value: securityContact });
26+
if (securityContact) {
27+
checkSingleLine(securityContact, 'securityContact');
28+
c.addContractMetadata({ key: 'security_contact', value: securityContact });
29+
}
1430

15-
if (license) c.license = license;
31+
if (license) {
32+
checkSingleLine(license, 'license');
33+
c.license = license;
34+
}
1635
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import test from 'ava';
2+
3+
import { ContractBuilder } from './contract';
4+
import { setInfo } from './set-info';
5+
import type { OptionsError } from './error';
6+
7+
const lineBreaks: [string, string][] = [
8+
['LF', '\n'],
9+
['CR', '\r'],
10+
['CRLF', '\r\n'],
11+
['LS', '\u2028'],
12+
['PS', '\u2029'],
13+
];
14+
15+
for (const [name, ch] of lineBreaks) {
16+
test(`setInfo rejects ${name} in securityContact`, t => {
17+
const c = new ContractBuilder('MyContract');
18+
const error = t.throws(() => setInfo(c, { securityContact: `security@example.com${ch}contract Injected {}` }));
19+
t.is((error as OptionsError).messages.securityContact, 'Must not contain line breaks');
20+
});
21+
22+
test(`setInfo rejects ${name} in license`, t => {
23+
const c = new ContractBuilder('MyContract');
24+
const error = t.throws(() => setInfo(c, { license: `MIT${ch}contract Injected {}` }));
25+
t.is((error as OptionsError).messages.license, 'Must not contain line breaks');
26+
});
27+
}
28+
29+
test('setInfo accepts valid single-line values', t => {
30+
const c = new ContractBuilder('MyContract');
31+
t.notThrows(() => setInfo(c, { securityContact: 'security@example.com', license: 'MIT' }));
32+
});

0 commit comments

Comments
 (0)