|
| 1 | +use anyhow::bail; |
1 | 2 | use serde::{Deserialize, Serialize}; |
2 | 3 | use std::fs; |
3 | 4 | use std::io::Write; |
| 5 | +#[cfg(unix)] |
| 6 | +use std::os::unix::fs::PermissionsExt; |
4 | 7 | use std::path::PathBuf; |
5 | 8 |
|
| 9 | +/// Validate that an ID is safe for use in filesystem paths. |
| 10 | +/// Rejects empty strings, path traversal (`..`), and characters outside `[a-zA-Z0-9._-]`. |
| 11 | +/// IDs longer than 256 characters are also rejected. |
| 12 | +pub fn validate_id(id: &str) -> anyhow::Result<()> { |
| 13 | + if id.is_empty() { |
| 14 | + bail!("ID must not be empty"); |
| 15 | + } |
| 16 | + if id.len() > 256 { |
| 17 | + bail!("ID must not exceed 256 characters"); |
| 18 | + } |
| 19 | + if id == "." || id == ".." { |
| 20 | + bail!("ID must not be '.' or '..'"); |
| 21 | + } |
| 22 | + if !id |
| 23 | + .chars() |
| 24 | + .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '_' || c == '-') |
| 25 | + { |
| 26 | + bail!("ID contains invalid characters (allowed: a-zA-Z0-9._-)"); |
| 27 | + } |
| 28 | + Ok(()) |
| 29 | +} |
| 30 | + |
6 | 31 | /// OCI User specification for UID/GID switching |
7 | 32 | #[derive(Debug, Deserialize, Serialize, Clone)] |
8 | 33 | pub struct OciUser { |
@@ -72,34 +97,49 @@ pub fn pid_path(id: &str) -> PathBuf { |
72 | 97 | } |
73 | 98 |
|
74 | 99 | pub fn save_state(state: &ContainerState) -> anyhow::Result<()> { |
| 100 | + validate_id(&state.id)?; |
75 | 101 | let dir = container_dir(&state.id); |
76 | 102 | fs::create_dir_all(&dir)?; |
| 103 | + #[cfg(unix)] |
| 104 | + fs::set_permissions(&dir, fs::Permissions::from_mode(0o700))?; |
| 105 | + let path = state_path(&state.id); |
77 | 106 | let json = serde_json::to_vec_pretty(&state)?; |
78 | | - fs::write(state_path(&state.id), json)?; |
| 107 | + fs::write(&path, json)?; |
| 108 | + #[cfg(unix)] |
| 109 | + fs::set_permissions(&path, fs::Permissions::from_mode(0o600))?; |
79 | 110 | Ok(()) |
80 | 111 | } |
81 | 112 |
|
82 | 113 | pub fn load_state(id: &str) -> anyhow::Result<ContainerState> { |
| 114 | + validate_id(id)?; |
83 | 115 | let data = fs::read(state_path(id))?; |
84 | 116 | let state: ContainerState = serde_json::from_slice(&data)?; |
85 | 117 | Ok(state) |
86 | 118 | } |
87 | 119 |
|
88 | 120 | pub fn save_pid(id: &str, pid: i32) -> anyhow::Result<()> { |
| 121 | + validate_id(id)?; |
89 | 122 | let dir = container_dir(id); |
90 | 123 | fs::create_dir_all(&dir)?; |
91 | | - let mut f = fs::File::create(pid_path(id))?; |
| 124 | + #[cfg(unix)] |
| 125 | + fs::set_permissions(&dir, fs::Permissions::from_mode(0o700))?; |
| 126 | + let path = pid_path(id); |
| 127 | + let mut f = fs::File::create(&path)?; |
92 | 128 | writeln!(f, "{}", pid)?; |
| 129 | + #[cfg(unix)] |
| 130 | + fs::set_permissions(&path, fs::Permissions::from_mode(0o600))?; |
93 | 131 | Ok(()) |
94 | 132 | } |
95 | 133 |
|
96 | 134 | pub fn load_pid(id: &str) -> anyhow::Result<i32> { |
| 135 | + validate_id(id)?; |
97 | 136 | let s = fs::read_to_string(pid_path(id))?; |
98 | 137 | let pid: i32 = s.trim().parse()?; |
99 | 138 | Ok(pid) |
100 | 139 | } |
101 | 140 |
|
102 | 141 | pub fn delete(id: &str) -> anyhow::Result<()> { |
| 142 | + validate_id(id)?; |
103 | 143 | let dir = container_dir(id); |
104 | 144 | if dir.exists() { |
105 | 145 | fs::remove_dir_all(dir)?; |
@@ -138,14 +178,23 @@ pub fn exec_state_path(container_id: &str, exec_id: &str) -> PathBuf { |
138 | 178 | } |
139 | 179 |
|
140 | 180 | pub fn save_exec_state(state: &ExecState) -> anyhow::Result<()> { |
| 181 | + validate_id(&state.container_id)?; |
| 182 | + validate_id(&state.exec_id)?; |
141 | 183 | let dir = container_dir(&state.container_id); |
142 | 184 | fs::create_dir_all(&dir)?; |
| 185 | + #[cfg(unix)] |
| 186 | + fs::set_permissions(&dir, fs::Permissions::from_mode(0o700))?; |
| 187 | + let path = exec_state_path(&state.container_id, &state.exec_id); |
143 | 188 | let json = serde_json::to_vec_pretty(&state)?; |
144 | | - fs::write(exec_state_path(&state.container_id, &state.exec_id), json)?; |
| 189 | + fs::write(&path, json)?; |
| 190 | + #[cfg(unix)] |
| 191 | + fs::set_permissions(&path, fs::Permissions::from_mode(0o600))?; |
145 | 192 | Ok(()) |
146 | 193 | } |
147 | 194 |
|
148 | 195 | pub fn load_exec_state(container_id: &str, exec_id: &str) -> anyhow::Result<ExecState> { |
| 196 | + validate_id(container_id)?; |
| 197 | + validate_id(exec_id)?; |
149 | 198 | let path = exec_state_path(container_id, exec_id); |
150 | 199 | let data = fs::read(&path)?; |
151 | 200 | let state: ExecState = serde_json::from_slice(&data)?; |
@@ -425,4 +474,52 @@ mod tests { |
425 | 474 | // .expect("Delete should not fail on nonexistent exec"); |
426 | 475 | // }); |
427 | 476 | // } |
| 477 | + |
| 478 | + // --- validate_id tests --- |
| 479 | + |
| 480 | + #[test] |
| 481 | + fn test_validate_id_valid() { |
| 482 | + assert!(validate_id("my-container").is_ok()); |
| 483 | + assert!(validate_id("abc123").is_ok()); |
| 484 | + assert!(validate_id("a.b_c-d").is_ok()); |
| 485 | + assert!(validate_id("A").is_ok()); |
| 486 | + } |
| 487 | + |
| 488 | + #[test] |
| 489 | + fn test_validate_id_rejects_empty() { |
| 490 | + assert!(validate_id("").is_err()); |
| 491 | + } |
| 492 | + |
| 493 | + #[test] |
| 494 | + fn test_validate_id_rejects_path_traversal() { |
| 495 | + assert!(validate_id("..").is_err()); |
| 496 | + assert!(validate_id("../etc/passwd").is_err()); |
| 497 | + assert!(validate_id("foo/../bar").is_err()); |
| 498 | + } |
| 499 | + |
| 500 | + #[test] |
| 501 | + fn test_validate_id_rejects_dot() { |
| 502 | + assert!(validate_id(".").is_err()); |
| 503 | + } |
| 504 | + |
| 505 | + #[test] |
| 506 | + fn test_validate_id_rejects_slashes() { |
| 507 | + assert!(validate_id("foo/bar").is_err()); |
| 508 | + assert!(validate_id("/absolute").is_err()); |
| 509 | + } |
| 510 | + |
| 511 | + #[test] |
| 512 | + fn test_validate_id_rejects_long() { |
| 513 | + let long_id = "a".repeat(257); |
| 514 | + assert!(validate_id(&long_id).is_err()); |
| 515 | + let ok_id = "a".repeat(256); |
| 516 | + assert!(validate_id(&ok_id).is_ok()); |
| 517 | + } |
| 518 | + |
| 519 | + #[test] |
| 520 | + fn test_validate_id_rejects_special_chars() { |
| 521 | + assert!(validate_id("foo bar").is_err()); |
| 522 | + assert!(validate_id("foo\nbar").is_err()); |
| 523 | + assert!(validate_id("foo\0bar").is_err()); |
| 524 | + } |
428 | 525 | } |
0 commit comments