Skip to content

Commit 72e4e95

Browse files
committed
feat: tedge diag collect security enhancement
If the process is run by root, the plugin must meet those conditions: * owned by root * not writable by other users Signed-off-by: Rina Fujino <rina.fujino.23@gmail.com>
1 parent bcb8c1b commit 72e4e95

File tree

4 files changed

+56
-10
lines changed

4 files changed

+56
-10
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/core/tedge/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ toml = { workspace = true }
6262
tracing = { workspace = true }
6363
tracing-subscriber = { workspace = true }
6464
url = { workspace = true }
65+
uzers = { workspace = true }
6566
which = { workspace = true }
6667
yansi = { workspace = true }
6768

crates/core/tedge/src/cli/diag/collect.rs

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use camino::Utf8PathBuf;
88
use flate2::write::GzEncoder;
99
use flate2::Compression;
1010
use std::collections::BTreeSet;
11+
use std::os::unix::fs::MetadataExt;
1112
use std::os::unix::fs::PermissionsExt;
1213
use std::process::ExitStatus;
1314
use std::time::Duration;
@@ -17,6 +18,7 @@ use tedge_config::models::AbsolutePath;
1718
use tedge_config::TEdgeConfig;
1819
use tedge_utils::file;
1920
use tracing::debug;
21+
use uzers;
2022

2123
#[derive(Debug)]
2224
pub struct DiagCollectCommand {
@@ -139,13 +141,9 @@ impl DiagCollectCommand {
139141

140142
while let Some(entry) = entries.next_entry().await? {
141143
if let Ok(path) = Utf8PathBuf::from_path_buf(entry.path()) {
142-
if path.extension() == Some("ignore") {
143-
debug!("Skipping ignored file: {:?}", entry.path());
144-
} else if path.is_file() && is_executable(&path).await {
144+
if validate_plugin(&path, logger).await {
145145
plugins.insert(path);
146146
continue;
147-
} else {
148-
logger.warning(&format!("Skipping non-executable file: {:?}", entry.path()));
149147
}
150148
} else {
151149
logger.warning(&format!("Ignoring invalid path: {:?}", entry.path()));
@@ -160,7 +158,7 @@ impl DiagCollectCommand {
160158
) -> Result<ExitStatus, anyhow::Error> {
161159
let plugin_name = plugin_path
162160
.file_stem()
163-
.with_context(|| format!("No file name for {}", plugin_path))?;
161+
.with_context(|| format!("No file name for {plugin_path}"))?;
164162
let plugin_output_dir = self.diag_dir.join(plugin_name);
165163
let output_file = plugin_output_dir.join("output.log");
166164
file::create_directory_with_defaults(&plugin_output_dir)
@@ -206,9 +204,40 @@ impl DiagCollectCommand {
206204
}
207205
}
208206

209-
async fn is_executable(path: &Utf8Path) -> bool {
207+
async fn validate_plugin(path: &Utf8Path, logger: &mut DualLogger) -> bool {
208+
if path.extension() == Some("ignore") {
209+
debug!("Skipping file: {path} (ignored file)");
210+
return false;
211+
}
212+
213+
if !path.is_file() {
214+
debug!("Skipping file: {path} (not a regular file)");
215+
return false;
216+
}
217+
210218
match tokio::fs::metadata(path).await {
211-
Ok(metadata) => metadata.permissions().mode() & 0o111 != 0,
219+
Ok(metadata) => {
220+
if metadata.permissions().mode() & 0o111 == 0 {
221+
logger.warning(&format!("Skipping file: {path} (not executable)"));
222+
return false;
223+
}
224+
225+
// extra metadata check when the process is running by the root user for the security
226+
if uzers::get_current_uid() == 0 {
227+
if metadata.uid() != 0 {
228+
logger.warning(&format!("Skipping file: {path} (not owned by root)"));
229+
return false;
230+
}
231+
if metadata.permissions().mode() & 0o022 != 0 {
232+
logger.warning(&format!(
233+
"Skipping file: {path} (writable by non-root users)",
234+
));
235+
return false;
236+
}
237+
}
238+
239+
true
240+
}
212241
Err(_) => false,
213242
}
214243
}

tests/RobotFramework/tests/tedge/diag/tedge_diag_collect.robot

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ Run tedge diag collect
2525

2626
Run tedge diag collect with multiple plugin directories
2727
Transfer To Device ${CURDIR}/00_template.sh /setup/diag-plugins/00_template.sh
28+
Execute Command chown root:root /setup/diag-plugins/00_template.sh
29+
2830
Execute Command
2931
... tedge diag collect --name tedge-diag-now --plugin-dir /usr/share/tedge/diag-plugins --plugin-dir /setup/diag-plugins
3032
... stdout=${False}
@@ -68,8 +70,21 @@ Invalid plugin is skipped
6870
... stdout=${False}
6971
... stderr=${True}
7072
${summary}= Execute Command cat /tmp/tedge-diag-invalid/summary.log
71-
Should Contain ${stderr} warning: Skipping
72-
Should Contain ${summary} warning: Skipping
73+
Should Contain ${stderr} (not executable)
74+
Should Contain ${summary} (not executable)
75+
76+
Extra security guard when the command is run by root
77+
Execute Command
78+
... touch /setup/diag-plugins/owned_by_non_root.sh && chown tedge:tedge /setup/diag-plugins/owned_by_non_root.sh && chmod +x /setup/diag-plugins/owned_by_non_root.sh
79+
Execute Command
80+
... sudo touch /setup/diag-plugins/writable_by_other_users.sh && chmod a+wx /setup/diag-plugins/writable_by_other_users.sh
81+
${stderr}= Execute Command
82+
... sudo tedge diag collect --plugin-dir /setup/diag-plugins
83+
... stdout=${False}
84+
... stderr=${True}
85+
... ignore_exit_code=${True}
86+
Should Contain ${stderr} owned_by_non_root.sh (not owned by root)
87+
Should Contain ${stderr} writable_by_other_users.sh (writable by non-root users)
7388

7489

7590
*** Keywords ***

0 commit comments

Comments
 (0)