Skip to content

Commit 1b3c083

Browse files
authored
Compiler version validation (#2303)
* more permissive compiler version in request body * Strip v from version in middleware and add tests * Remove unused SolidityCompiler schema in apiv2.yaml
1 parent 2714dad commit 1b3c083

File tree

3 files changed

+228
-5
lines changed

3 files changed

+228
-5
lines changed

services/server/src/apiv2.yaml

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@ components:
312312
313313
Must include the `language` field inside the stdJsonInput object. Currently supports Solidity and Vyper.
314314
compilerVersion:
315-
$ref: '#/components/schemas/SolidityCompilerVersion'
315+
$ref: '#/components/schemas/CompilerVersion'
316316
contractIdentifier:
317317
type: string
318318
description: The fully qualified file path and contract name to indicate which contract to verify.
@@ -581,7 +581,8 @@ components:
581581
type: string
582582
example: 'solc'
583583
compilerVersion:
584-
$ref: '#/components/schemas/SolidityCompilerVersion'
584+
$ref: '#/components/schemas/CompilerVersion'
585+
description: 'The server accepts v prefixed but should only respond without the v prefix'
585586
compilerSettings:
586587
type: object
587588
name:
@@ -1114,10 +1115,14 @@ components:
11141115
- customCode: unsupported_chain
11151116
message: The chain with chainId 9429413 is not supported
11161117
errorId: 1ac6b91a-0605-4459-93dc-18f210a70192
1117-
SolidityCompilerVersion:
1118+
CompilerVersion:
11181119
type: string
1119-
title: SolidityCompilerVersion
1120-
pattern: '\d+\.\d+\.\d+(-nightly\.\d{4}\.\d+\.\d+)?\+commit\.[a-f0-9]{8}'
1120+
title: CompilerVersion
1121+
# Pattern accepts both Solidity and Vyper version formats:
1122+
# We need to be permissive here because Vyper versions are named inconsistently.
1123+
# - Solidity: 0.8.7+commit.e28d00a7 (8-char commit hash)
1124+
# - Vyper: v0.3.10, 0.3.8+commit.036f1536, v0.4.1rc1, etc.
1125+
pattern: '^v?\d+\.\d+\.\d+.*$'
11211126
example: '0.8.7+commit.e28d00a7'
11221127
Keccak256:
11231128
type: string

services/server/src/server/apiv2/middlewares.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,39 @@ export function validateFieldsAndOmit(
105105
next();
106106
}
107107

108+
export function validateCompilerVersion(
109+
req: Request,
110+
res: Response,
111+
next: NextFunction,
112+
) {
113+
let compilerVersion = req.body.compilerVersion;
114+
if (!compilerVersion) {
115+
throw new InvalidParametersError("Compiler version is required.");
116+
}
117+
118+
if (compilerVersion.startsWith("v")) {
119+
compilerVersion = compilerVersion.slice(1);
120+
}
121+
122+
// Validate based on language if available
123+
const language = req.body.stdJsonInput?.language;
124+
if (language === "Solidity") {
125+
// Solidity version pattern: 0.8.7+commit.e28d00a7 or 0.8.31-nightly.2025.8.11+commit.635fe8f8
126+
const solidityPattern =
127+
/^\d+\.\d+\.\d+(-nightly\.\d{4}\.\d+\.\d+)?\+commit\.[a-f0-9]{8}$/;
128+
if (!solidityPattern.test(compilerVersion)) {
129+
throw new InvalidParametersError(
130+
`Invalid Solidity compiler version format: ${compilerVersion}. Expected format: x.y.z+commit.xxxxxxxx or x.y.z-nightly.yyyy.m.d+commit.xxxxxxxx`,
131+
);
132+
}
133+
}
134+
// For Vyper and other languages, we can't do much validation here due to inconsistent naming.
135+
// It will throw if it can't download the version.
136+
137+
req.body.compilerVersion = compilerVersion;
138+
next();
139+
}
140+
108141
export function validateStandardJsonInput(
109142
req: Request,
110143
res: Response,
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
import { Request, Response } from "express";
2+
import { expect } from "chai";
3+
import * as sinon from "sinon";
4+
import { validateCompilerVersion } from "../../../src/server/apiv2/middlewares";
5+
import { InvalidParametersError } from "../../../src/server/apiv2/errors";
6+
7+
describe("validateCompilerVersion middleware", () => {
8+
let req: Partial<Request>;
9+
let res: Partial<Response>;
10+
let next: sinon.SinonSpy;
11+
12+
beforeEach(() => {
13+
req = {
14+
body: {},
15+
};
16+
res = {};
17+
next = sinon.spy();
18+
});
19+
20+
// Test helper functions
21+
const testValidVersions = (
22+
versions: string[],
23+
language: string,
24+
description: string,
25+
) => {
26+
it(description, () => {
27+
versions.forEach((version) => {
28+
next.resetHistory();
29+
const originalVersion = version;
30+
31+
req.body = {
32+
compilerVersion: version,
33+
stdJsonInput: { language },
34+
};
35+
36+
validateCompilerVersion(req as Request, res as Response, next);
37+
38+
// Check that v prefix is stripped
39+
const expectedVersion = originalVersion.startsWith("v")
40+
? originalVersion.slice(1)
41+
: originalVersion;
42+
43+
expect(req.body.compilerVersion).to.equal(expectedVersion);
44+
expect(next.calledOnce).to.be.true;
45+
});
46+
});
47+
};
48+
49+
const testInvalidVersions = (
50+
versions: string[],
51+
language: string,
52+
description: string,
53+
) => {
54+
it(description, () => {
55+
versions.forEach((version) => {
56+
req.body = {
57+
compilerVersion: version,
58+
stdJsonInput: { language },
59+
};
60+
61+
try {
62+
validateCompilerVersion(req as Request, res as Response, next);
63+
expect.fail(`Expected ${version} to throw an error but it didn't`);
64+
} catch (error: any) {
65+
expect(error).to.be.instanceOf(InvalidParametersError);
66+
expect(error.statusCode).to.equal(400);
67+
expect(error.payload.customCode).to.equal("invalid_parameter");
68+
}
69+
});
70+
});
71+
};
72+
73+
describe("Basic validation", () => {
74+
it("should throw error when compilerVersion is missing", () => {
75+
req.body = {};
76+
77+
try {
78+
validateCompilerVersion(req as Request, res as Response, next);
79+
expect.fail("Expected function to throw an error but it didn't");
80+
} catch (error: any) {
81+
expect(error).to.be.instanceOf(InvalidParametersError);
82+
expect(error.statusCode).to.equal(400);
83+
expect(error.payload.customCode).to.equal("invalid_parameter");
84+
}
85+
});
86+
87+
it("should throw error when compilerVersion is empty string", () => {
88+
req.body = { compilerVersion: "" };
89+
90+
try {
91+
validateCompilerVersion(req as Request, res as Response, next);
92+
expect.fail("Expected function to throw an error but it didn't");
93+
} catch (error: any) {
94+
expect(error).to.be.instanceOf(InvalidParametersError);
95+
expect(error.statusCode).to.equal(400);
96+
expect(error.payload.customCode).to.equal("invalid_parameter");
97+
}
98+
});
99+
100+
it("should strip 'v' prefix from version", () => {
101+
req.body = {
102+
compilerVersion: "v0.8.7+commit.e28d00a7",
103+
stdJsonInput: { language: "Solidity" },
104+
};
105+
106+
validateCompilerVersion(req as Request, res as Response, next);
107+
108+
expect(req.body.compilerVersion).to.equal("0.8.7+commit.e28d00a7");
109+
expect(next.calledOnce).to.be.true;
110+
});
111+
});
112+
113+
describe("Solidity version validation", () => {
114+
const validSolidityVersions = [
115+
// Regular versions
116+
"0.8.7+commit.e28d00a7",
117+
"0.8.30+commit.73712a01",
118+
"0.8.29+commit.ab55807c",
119+
"0.8.12+commit.f00d7308",
120+
"0.8.31+commit.73712a01",
121+
// Nightly versions
122+
"0.8.31-nightly.2025.8.11+commit.635fe8f8",
123+
"0.8.31-nightly.2025.7.31+commit.aef512f8",
124+
"0.8.31-nightly.2025.6.2+commit.5c3c9578",
125+
// With v prefix (should be stripped)
126+
"v0.8.20+commit.a1b79de6",
127+
"v0.8.7+commit.e28d00a7",
128+
];
129+
130+
const invalidSolidityVersions = [
131+
// Wrong commit hash length
132+
"0.8.7+commit.e28d00a", // too short
133+
"0.8.7+commit.e28d00a7123", // too long
134+
// Invalid commit hash characters
135+
"0.8.7+commit.g28d00a7", // contains 'g'
136+
"0.8.7+commit.E28D00A7", // uppercase
137+
"0.8.7+commit.e28D00A7", // mixed case
138+
// Missing commit hash
139+
"0.8.7",
140+
"0.8.31-nightly.2025.8.11",
141+
// Malformed nightly format
142+
"0.8.31-nightly.25.8.11+commit.635fe8f8", // 2-digit year
143+
"0.8.31-nightly.2025.8+commit.635fe8f8", // missing day
144+
// Invalid version format
145+
"invalid-version",
146+
"0.8+commit.e28d00a7",
147+
];
148+
149+
testValidVersions(
150+
validSolidityVersions,
151+
"Solidity",
152+
"should accept all valid Solidity versions",
153+
);
154+
155+
testInvalidVersions(
156+
invalidSolidityVersions,
157+
"Solidity",
158+
"should reject all invalid Solidity versions",
159+
);
160+
});
161+
162+
describe("Vyper version validation", () => {
163+
const validVyperVersions = [
164+
// With v prefix
165+
"v0.3.10",
166+
"v0.4.1rc1",
167+
"v0.4.1b4",
168+
"v0.1.0-beta.17",
169+
// Without v prefix
170+
"0.3.8+commit.036f1536",
171+
"0.4.2+commit.c216787f",
172+
"0.3.10",
173+
// Various suffixes
174+
"0.4.1rc1",
175+
"0.4.1b4",
176+
"0.1.0-beta.17",
177+
];
178+
179+
testValidVersions(
180+
validVyperVersions,
181+
"Vyper",
182+
"should accept all Vyper versions (permissive validation)",
183+
);
184+
});
185+
});

0 commit comments

Comments
 (0)