Fix audit findings: path traversal, exec shell, glob dup, dead bread-sync, version drift
- modules_mgmt.rs: reject module names containing path separators, `..`,
or absolute paths before joining onto modules_dir (install_from_local,
remove_module, read_module_manifest); adds canonicalized containment
check as defense in depth. Manifest-supplied names and CLI args were
previously joined unsanitized, allowing path traversal on install/remove.
- breadd/src/lua/mod.rs: bread.exec now runs via `sh -c` instead of
`sh -lc`; no documented reason was found for login-shell semantics.
- Unify the two independently hand-written glob matchers (subscription
dispatch in breadd/src/core/subscriptions.rs vs. the CLI --filter path
in breadd/src/ipc/mod.rs) into one implementation in
bread-shared/src/glob.rs, used by both call sites.
- Remove the dead bread-sync/ tree (already excluded from the workspace
and fully unreferenced) and its stale PKGBUILD deps (libgit2, git
optdepend) and packaging docs mention.
- Correct the version-number transposition bug ("6.2.0" instead of
"0.6.2"/"0.6.6") across bread-shared, breadd, and bread-cli Cargo.toml,
and fix PKGBUILD's stale pkgver, so Cargo.toml/doctor/PKGBUILD all agree
with the latest git tag (v0.6.6).
This commit is contained in:
parent
1fda781b4c
commit
89c5849539
25 changed files with 319 additions and 3085 deletions
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "breadd"
|
||||
version = "6.2.0"
|
||||
version = "0.6.6"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
|
|
|
|||
|
|
@ -60,147 +60,22 @@ impl SubscriptionTable {
|
|||
pub fn match_event(&self, event_name: &str) -> Vec<Subscription> {
|
||||
self.entries
|
||||
.iter()
|
||||
.filter(|sub| matches_pattern(&sub.pattern, event_name))
|
||||
.filter(|sub| bread_shared::glob::matches_pattern(&sub.pattern, event_name))
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
fn matches_pattern(pattern: &str, event_name: &str) -> bool {
|
||||
if let Some(prefix) = pattern.strip_suffix(".**") {
|
||||
if event_name == prefix {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
matches_glob(pattern.as_bytes(), event_name.as_bytes())
|
||||
}
|
||||
|
||||
fn matches_glob(pattern: &[u8], text: &[u8]) -> bool {
|
||||
if pattern.is_empty() {
|
||||
return text.is_empty();
|
||||
}
|
||||
|
||||
if pattern.len() >= 2 && pattern[0] == b'*' && pattern[1] == b'*' {
|
||||
let mut idx = 2;
|
||||
while pattern.len() >= idx + 2 && pattern[idx] == b'*' && pattern[idx + 1] == b'*' {
|
||||
idx += 2;
|
||||
}
|
||||
let rest = &pattern[idx..];
|
||||
if rest.is_empty() {
|
||||
return true;
|
||||
}
|
||||
for offset in 0..=text.len() {
|
||||
if matches_glob(rest, &text[offset..]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
match pattern[0] {
|
||||
b'*' => {
|
||||
let mut offset = 0;
|
||||
loop {
|
||||
if matches_glob(&pattern[1..], &text[offset..]) {
|
||||
return true;
|
||||
}
|
||||
if offset == text.len() || text[offset] == b'.' {
|
||||
break;
|
||||
}
|
||||
offset += 1;
|
||||
}
|
||||
false
|
||||
}
|
||||
b'?' => {
|
||||
if text.is_empty() || text[0] == b'.' {
|
||||
return false;
|
||||
}
|
||||
matches_glob(&pattern[1..], &text[1..])
|
||||
}
|
||||
ch => {
|
||||
if text.first().copied() != Some(ch) {
|
||||
return false;
|
||||
}
|
||||
matches_glob(&pattern[1..], &text[1..])
|
||||
}
|
||||
}
|
||||
}
|
||||
// Glob-matching semantics (`*`, `**`, `?`) are implemented and tested once,
|
||||
// in `bread_shared::glob`. Both this module (real event dispatch) and
|
||||
// `breadd::ipc` (the CLI `--filter` path) delegate to that single
|
||||
// implementation so they cannot drift apart. See `bread-shared/src/glob.rs`
|
||||
// for the pattern-matching test suite.
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn exact_match() {
|
||||
assert!(matches_pattern(
|
||||
"bread.device.dock.connected",
|
||||
"bread.device.dock.connected"
|
||||
));
|
||||
assert!(!matches_pattern(
|
||||
"bread.device.dock.connected",
|
||||
"bread.device.dock.disconnected"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_segment_wildcard() {
|
||||
assert!(matches_pattern("bread.device.*", "bread.device.foo"));
|
||||
assert!(!matches_pattern(
|
||||
"bread.device.*",
|
||||
"bread.device.dock.connected"
|
||||
));
|
||||
assert!(!matches_pattern("bread.device.*", "bread.device"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recursive_wildcard() {
|
||||
assert!(matches_pattern(
|
||||
"bread.device.**",
|
||||
"bread.device.dock.connected"
|
||||
));
|
||||
assert!(matches_pattern("bread.**", "bread.device.dock.connected"));
|
||||
assert!(matches_pattern("bread.**", "bread"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_char_wildcard() {
|
||||
assert!(matches_pattern("bread.monitor.?", "bread.monitor.1"));
|
||||
assert!(!matches_pattern("bread.monitor.?", "bread.monitor.10"));
|
||||
assert!(!matches_pattern("bread.monitor.?", "bread.monitor."));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn star_does_not_cross_dot_segments() {
|
||||
// `*` matches within a segment only.
|
||||
assert!(matches_pattern(
|
||||
"bread.*.connected",
|
||||
"bread.device.connected"
|
||||
));
|
||||
assert!(!matches_pattern(
|
||||
"bread.*.connected",
|
||||
"bread.device.dock.connected"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn double_star_matches_zero_or_more_segments() {
|
||||
assert!(matches_pattern("bread.**", "bread.a"));
|
||||
assert!(matches_pattern("bread.**", "bread.a.b.c.d"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_pattern_matches_only_empty_text() {
|
||||
assert!(matches_pattern("", ""));
|
||||
assert!(!matches_pattern("", "bread"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_text_only_matches_wildcards() {
|
||||
assert!(matches_pattern("**", ""));
|
||||
assert!(!matches_pattern("bread.*", ""));
|
||||
}
|
||||
|
||||
// ─── SubscriptionTable ────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -323,7 +323,7 @@ impl Server {
|
|||
loop {
|
||||
let evt = rx.recv().await?;
|
||||
if let Some(filter) = filter.as_deref() {
|
||||
if !matches_filter(&evt.event, filter) {
|
||||
if !bread_shared::glob::matches_pattern(filter, &evt.event) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
|
@ -337,127 +337,9 @@ impl Server {
|
|||
}
|
||||
}
|
||||
|
||||
fn matches_filter(event_name: &str, pattern: &str) -> bool {
|
||||
// Delegates to the same glob logic as the subscription table:
|
||||
// `*` matches one segment (no dot-crossing), `**` matches any depth.
|
||||
if let Some(prefix) = pattern.strip_suffix(".**") {
|
||||
if event_name == prefix || event_name.starts_with(&format!("{prefix}.")) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
matches_glob_filter(pattern.as_bytes(), event_name.as_bytes())
|
||||
}
|
||||
|
||||
fn matches_glob_filter(pattern: &[u8], text: &[u8]) -> bool {
|
||||
if pattern.is_empty() {
|
||||
return text.is_empty();
|
||||
}
|
||||
|
||||
if pattern.len() >= 2 && pattern[0] == b'*' && pattern[1] == b'*' {
|
||||
let rest = &pattern[2..];
|
||||
if rest.is_empty() {
|
||||
return true;
|
||||
}
|
||||
for offset in 0..=text.len() {
|
||||
if matches_glob_filter(rest, &text[offset..]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
match pattern[0] {
|
||||
b'*' => {
|
||||
let mut offset = 0;
|
||||
loop {
|
||||
if matches_glob_filter(&pattern[1..], &text[offset..]) {
|
||||
return true;
|
||||
}
|
||||
if offset == text.len() || text[offset] == b'.' {
|
||||
break;
|
||||
}
|
||||
offset += 1;
|
||||
}
|
||||
false
|
||||
}
|
||||
b'?' => {
|
||||
if text.is_empty() || text[0] == b'.' {
|
||||
return false;
|
||||
}
|
||||
matches_glob_filter(&pattern[1..], &text[1..])
|
||||
}
|
||||
ch => {
|
||||
if text.first().copied() != Some(ch) {
|
||||
return false;
|
||||
}
|
||||
matches_glob_filter(&pattern[1..], &text[1..])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::matches_filter;
|
||||
|
||||
#[test]
|
||||
fn filter_exact_match() {
|
||||
assert!(matches_filter("bread.window.opened", "bread.window.opened"));
|
||||
assert!(!matches_filter(
|
||||
"bread.window.opened",
|
||||
"bread.window.closed"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filter_dot_star_matches_one_segment_only() {
|
||||
assert!(matches_filter("bread.device.connected", "bread.device.*"));
|
||||
assert!(!matches_filter(
|
||||
"bread.device.dock.connected",
|
||||
"bread.device.*"
|
||||
));
|
||||
assert!(!matches_filter("bread.device", "bread.device.*"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filter_dot_double_star_matches_zero_or_more_segments() {
|
||||
// Matches the exact prefix (zero segments after).
|
||||
assert!(matches_filter("bread.device", "bread.device.**"));
|
||||
// And matches deeper paths.
|
||||
assert!(matches_filter(
|
||||
"bread.device.dock.connected",
|
||||
"bread.device.**"
|
||||
));
|
||||
// But not a sibling at the same depth.
|
||||
assert!(!matches_filter(
|
||||
"bread.network.connected",
|
||||
"bread.device.**"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filter_question_mark_matches_single_char_not_dot() {
|
||||
assert!(matches_filter("bread.x", "bread.?"));
|
||||
assert!(!matches_filter("bread.xy", "bread.?"));
|
||||
assert!(!matches_filter("bread.", "bread.?"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filter_mid_pattern_star_does_not_cross_dots() {
|
||||
// A `*` in the middle of the pattern (not the `.*` suffix shortcut)
|
||||
// matches within a single segment only.
|
||||
assert!(matches_filter("bread.alpha.connected", "bread.*.connected"));
|
||||
assert!(!matches_filter(
|
||||
"bread.alpha.beta.connected",
|
||||
"bread.*.connected"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filter_dot_star_matches_exactly_one_segment() {
|
||||
assert!(matches_filter("bread.alpha", "bread.*"));
|
||||
assert!(!matches_filter("bread.alpha.beta", "bread.*"));
|
||||
assert!(!matches_filter("bread", "bread.*"));
|
||||
}
|
||||
}
|
||||
// The CLI `--filter` glob semantics used to be a second, hand-rolled copy of
|
||||
// the subscription-table matcher (`matches_filter`/`matches_glob_filter`
|
||||
// used to live here). That duplication is exactly what let the two paths
|
||||
// drift out of sync despite the docs claiming parity. Both now delegate to
|
||||
// the single implementation in `bread_shared::glob::matches_pattern`; see
|
||||
// that module for the pattern-matching test suite.
|
||||
|
|
|
|||
|
|
@ -540,7 +540,7 @@ impl LuaEngine {
|
|||
let exec_fn = self.lua.create_function(move |_lua, cmd: String| {
|
||||
task::spawn_blocking(move || {
|
||||
match std::process::Command::new("sh")
|
||||
.arg("-lc")
|
||||
.arg("-c")
|
||||
.arg(&cmd)
|
||||
.status()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue