Skip to content

Commit 53ec861

Browse files
authored
fix(contract-verifier): allow relative imports (#4761)
1 parent ad7a55d commit 53ec861

File tree

4 files changed

+336
-8
lines changed

4 files changed

+336
-8
lines changed

core/lib/contract_verifier/src/compilers/mod.rs

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,15 +72,18 @@ pub(crate) fn validate_source_paths(
7272
}
7373

7474
/// Returns `true` if `source` contains an `import` directive whose path is absolute (`/…`)
75-
/// or starts with a parent-directory traversal (`../…`). Both forms let the compiler
76-
/// resolve imports against the host filesystem and leak file contents in error messages.
75+
/// or uses a `file://` URL. These forms let the compiler resolve imports against the host
76+
/// filesystem and potentially leak file contents in error messages.
77+
///
78+
/// Relative imports containing `../` are allowed: they are standard Solidity practice and
79+
/// remain sandboxed by `validate_source_paths()` plus the empty compiler search directory.
7780
pub(crate) fn has_dangerous_imports(source: &str) -> bool {
7881
// Covers all Solidity import forms:
7982
// import "/path";
80-
// import "../path";
8183
// import {X} from "/path";
8284
// import * as X from "/path";
83-
let re = Regex::new(r#"\bimport\b[^;]*?["'](/|\.\.)"#).unwrap();
85+
// import "file:///path";
86+
let re = Regex::new(r#"\bimport\b[^;]*?["'](?:/|file://)"#).unwrap();
8487
re.is_match(source)
8588
}
8689

@@ -114,6 +117,30 @@ pub(crate) fn sanitize_compiler_stderr(stderr: &str) -> String {
114117
.join("\n")
115118
}
116119

120+
#[cfg(test)]
121+
mod tests {
122+
use super::has_dangerous_imports;
123+
124+
#[test]
125+
fn allows_relative_parent_imports() {
126+
let source = r#"
127+
import {ContextUpgradeable} from "../utils/ContextUpgradeable.sol";
128+
import {Hashes} from "./Hashes.sol";
129+
"#;
130+
131+
assert!(
132+
!has_dangerous_imports(source),
133+
"relative imports within the submitted source tree must be allowed"
134+
);
135+
}
136+
137+
#[test]
138+
fn rejects_absolute_imports() {
139+
assert!(has_dangerous_imports(r#"import "/etc/shadow";"#));
140+
assert!(has_dangerous_imports(r#"import "file:///etc/shadow";"#));
141+
}
142+
}
143+
117144
/// Users may provide either just contract name or source file name and contract name joined with ":".
118145
fn process_contract_name(original_name: &str, extension: &str) -> (String, String) {
119146
if let Some((file_name, contract_name)) = original_name.rsplit_once(':') {

core/lib/contract_verifier/src/compilers/solc.rs

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ impl Solc {
4949
SourceCodeData::SolSingleFile(source_code) => {
5050
if has_dangerous_imports(&source_code) {
5151
return Err(ContractVerifierError::InvalidSourcePath(
52-
"import with absolute or traversal path".to_owned(),
52+
"import with absolute path".to_owned(),
5353
));
5454
}
5555
let source = Source {
@@ -85,7 +85,7 @@ impl Solc {
8585
for source in compiler_input.sources.values() {
8686
if has_dangerous_imports(&source.content) {
8787
return Err(ContractVerifierError::InvalidSourcePath(
88-
"import with absolute or traversal path".to_owned(),
88+
"import with absolute path".to_owned(),
8989
));
9090
}
9191
}
@@ -123,6 +123,105 @@ impl Solc {
123123
}
124124
}
125125

126+
#[cfg(test)]
127+
mod tests {
128+
use zksync_types::contract_verification::api::CompilerVersions;
129+
130+
use super::*;
131+
132+
#[test]
133+
fn build_input_allows_relative_parent_imports_in_standard_json() {
134+
let input = serde_json::json!({
135+
"language": "Solidity",
136+
"sources": {
137+
"src/Counter.sol": {
138+
"content": r#"
139+
pragma solidity ^0.8.20;
140+
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
141+
142+
contract Counter is OwnableUpgradeable {
143+
function initialize(address owner) external initializer {
144+
__Ownable_init(owner);
145+
}
146+
}
147+
"#,
148+
},
149+
"@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol": {
150+
"content": r#"
151+
pragma solidity ^0.8.20;
152+
import "../utils/ContextUpgradeable.sol";
153+
import "@openzeppelin/contracts/proxy/utils/Initializable.sol";
154+
155+
abstract contract OwnableUpgradeable is Initializable, ContextUpgradeable {
156+
address private _owner;
157+
158+
function __Ownable_init(address initialOwner) internal onlyInitializing {
159+
_owner = initialOwner;
160+
}
161+
}
162+
"#,
163+
},
164+
"@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol": {
165+
"content": r#"
166+
pragma solidity ^0.8.20;
167+
import "@openzeppelin/contracts/proxy/utils/Initializable.sol";
168+
169+
abstract contract ContextUpgradeable is Initializable {
170+
function _msgSender() internal view virtual returns (address) {
171+
return msg.sender;
172+
}
173+
}
174+
"#,
175+
},
176+
"@openzeppelin/contracts/proxy/utils/Initializable.sol": {
177+
"content": r#"
178+
pragma solidity ^0.8.20;
179+
180+
abstract contract Initializable {
181+
modifier initializer() {
182+
_;
183+
}
184+
185+
modifier onlyInitializing() {
186+
_;
187+
}
188+
}
189+
"#,
190+
},
191+
},
192+
"settings": {
193+
"optimizer": {
194+
"enabled": true,
195+
},
196+
},
197+
});
198+
let req = VerificationIncomingRequest {
199+
contract_address: Default::default(),
200+
source_code_data: SourceCodeData::StandardJsonInput(input.as_object().unwrap().clone()),
201+
contract_name: "src/Counter.sol:Counter".to_owned(),
202+
compiler_versions: CompilerVersions::Solc {
203+
compiler_solc_version: "0.8.26".to_owned(),
204+
compiler_zksolc_version: None,
205+
},
206+
optimization_used: true,
207+
optimizer_mode: None,
208+
constructor_arguments: Default::default(),
209+
is_system: false,
210+
force_evmla: false,
211+
evm_specific: Default::default(),
212+
};
213+
214+
let built = Solc::build_input(req).expect("relative parent imports should be allowed");
215+
216+
assert_eq!(built.file_name, "src/Counter.sol");
217+
assert_eq!(built.contract_name, "Counter");
218+
assert!(built
219+
.standard_json
220+
.sources
221+
.contains_key("@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"));
222+
}
223+
}
224+
126225
#[async_trait]
127226
impl Compiler<SolcInput> for Solc {
128227
async fn compile(
@@ -141,6 +240,7 @@ impl Compiler<SolcInput> for Solc {
141240
.arg("--standard-json")
142241
.arg("--allow-paths")
143242
.arg(compile_dir.path())
243+
.current_dir(compile_dir.path())
144244
.stdin(Stdio::piped())
145245
.stdout(Stdio::piped())
146246
.stderr(Stdio::piped())

core/lib/contract_verifier/src/compilers/zksolc.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ impl ZkSolc {
101101
SourceCodeData::SolSingleFile(source_code) => {
102102
if has_dangerous_imports(&source_code) {
103103
return Err(ContractVerifierError::InvalidSourcePath(
104-
"import with absolute or traversal path".to_owned(),
104+
"import with absolute path".to_owned(),
105105
));
106106
}
107107
let source = Source {
@@ -138,7 +138,7 @@ impl ZkSolc {
138138
for source in compiler_input.sources.values() {
139139
if has_dangerous_imports(&source.content) {
140140
return Err(ContractVerifierError::InvalidSourcePath(
141-
"import with absolute or traversal path".to_owned(),
141+
"import with absolute path".to_owned(),
142142
));
143143
}
144144
}

0 commit comments

Comments
 (0)