mirror of
https://github.com/samvallad33/vestige.git
synced 2026-07-22 23:31:02 +02:00
fix(audit): round-2 panics, overflows, XML/SSRF hardening (swarm)
Second swarm pass (complete every-line sweep), verified against real code (38/101 confirmed real; 63 false positives excluded). This commit lands the main-compatible subset: - redmine SSRF guard: rewrite host check to use host_str()+std::net::IpAddr instead of the `url` crate (url is not a direct dep of vestige-core on main; the previous form only compiled on the feature branch). Same protection: blocks localhost + loopback/private/link-local/unspecified IPs. - bin/restore: guard wrapper[0] index (empty backup array would panic) - bin/cli: char-boundary-safe node.id truncation (2 byte-slice panics); XML-escape the model/home strings before launchd plist substitution - prospective_memory: Duration::try_hours/try_days (panic on out-of-range user config); case-insensitive " at " split now uses the lowercased index so the contains() check and the split agree core 477/0, mcp builds, clippy clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0b368b7e58
commit
8900a27c40
3 changed files with 34 additions and 13 deletions
|
|
@ -988,10 +988,11 @@ impl IntentionParser {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for "at X" time pattern
|
// Check for "at X" time pattern. Split using the byte index found in the
|
||||||
if text_lower.contains(" at ") {
|
// lowercased text so the case-insensitive contains() and the split agree
|
||||||
// For now, treat as a simple event trigger
|
// (otherwise "Meeting AT 5pm" passes the check but fails the split).
|
||||||
let parts: Vec<&str> = original.splitn(2, " at ").collect();
|
if let Some(idx) = text_lower.find(" at ") {
|
||||||
|
let parts: Vec<&str> = vec![&original[..idx], &original[idx + 4..]];
|
||||||
if parts.len() == 2 {
|
if parts.len() == 2 {
|
||||||
let part0_lower = parts[0].to_lowercase();
|
let part0_lower = parts[0].to_lowercase();
|
||||||
let content: String = if part0_lower.starts_with("remind me to ") {
|
let content: String = if part0_lower.starts_with("remind me to ") {
|
||||||
|
|
@ -1292,7 +1293,11 @@ impl ProspectiveMemory {
|
||||||
|
|
||||||
// Check for deadline escalation
|
// Check for deadline escalation
|
||||||
if self.config.enable_escalation {
|
if self.config.enable_escalation {
|
||||||
let threshold = Duration::hours(self.config.escalation_threshold_hours);
|
// try_hours avoids the panic Duration::hours has on an
|
||||||
|
// out-of-range (user-configurable) value; fall back to a large
|
||||||
|
// but safe default if the config is absurd.
|
||||||
|
let threshold = Duration::try_hours(self.config.escalation_threshold_hours)
|
||||||
|
.unwrap_or_else(|| Duration::hours(24));
|
||||||
if intention.is_deadline_approaching(threshold) {
|
if intention.is_deadline_approaching(threshold) {
|
||||||
// Priority will be automatically escalated via effective_priority()
|
// Priority will be automatically escalated via effective_priority()
|
||||||
}
|
}
|
||||||
|
|
@ -1349,9 +1354,11 @@ impl ProspectiveMemory {
|
||||||
|
|
||||||
history.push_back(fulfilled_intention);
|
history.push_back(fulfilled_intention);
|
||||||
|
|
||||||
// Maintain history size
|
// Maintain history size. try_days avoids the panic on an
|
||||||
let retention_cutoff =
|
// out-of-range user-configurable retention value.
|
||||||
Utc::now() - Duration::days(self.config.completed_retention_days);
|
let retention_window = Duration::try_days(self.config.completed_retention_days)
|
||||||
|
.unwrap_or_else(|| Duration::days(30));
|
||||||
|
let retention_cutoff = Utc::now() - retention_window;
|
||||||
while history
|
while history
|
||||||
.front()
|
.front()
|
||||||
.map(|i| i.fulfilled_at.unwrap_or(i.created_at) < retention_cutoff)
|
.map(|i| i.fulfilled_at.unwrap_or(i.created_at) < retention_cutoff)
|
||||||
|
|
|
||||||
|
|
@ -829,9 +829,19 @@ fn install_launchd_job(source_root: &Path, home: &Path, model: &str) -> anyhow::
|
||||||
.join("com.vestige.mlx-server.plist.template");
|
.join("com.vestige.mlx-server.plist.template");
|
||||||
let template = fs::read_to_string(&template_path)
|
let template = fs::read_to_string(&template_path)
|
||||||
.with_context(|| format!("failed to read {}", template_path.display()))?;
|
.with_context(|| format!("failed to read {}", template_path.display()))?;
|
||||||
|
// XML-escape interpolated values: this plist is XML, and an unescaped model
|
||||||
|
// string containing &, <, >, " or ' would corrupt the plist (or inject
|
||||||
|
// elements). Escape before substitution.
|
||||||
|
let xml_escape = |s: &str| {
|
||||||
|
s.replace('&', "&")
|
||||||
|
.replace('<', "<")
|
||||||
|
.replace('>', ">")
|
||||||
|
.replace('"', """)
|
||||||
|
.replace('\'', "'")
|
||||||
|
};
|
||||||
let rendered = template
|
let rendered = template
|
||||||
.replace("__HOME__", &home.display().to_string())
|
.replace("__HOME__", &xml_escape(&home.display().to_string()))
|
||||||
.replace("__MODEL__", model);
|
.replace("__MODEL__", &xml_escape(model));
|
||||||
|
|
||||||
let plist = launchd_dir.join("com.vestige.mlx-server.plist");
|
let plist = launchd_dir.join("com.vestige.mlx-server.plist");
|
||||||
fs::write(&plist, rendered)?;
|
fs::write(&plist, rendered)?;
|
||||||
|
|
@ -2452,7 +2462,7 @@ fn run_gc(
|
||||||
let age_days = (now - node.created_at).num_days();
|
let age_days = (now - node.created_at).num_days();
|
||||||
println!(
|
println!(
|
||||||
" {} [ret={:.3}, age={}d] {}",
|
" {} [ret={:.3}, age={}d] {}",
|
||||||
node.id[..8].dimmed(),
|
node.id.get(..8).unwrap_or(&node.id).dimmed(),
|
||||||
node.retention_strength,
|
node.retention_strength,
|
||||||
age_days,
|
age_days,
|
||||||
truncate(&node.content, 60).dimmed()
|
truncate(&node.content, 60).dimmed()
|
||||||
|
|
@ -2513,7 +2523,7 @@ fn run_gc(
|
||||||
eprintln!(
|
eprintln!(
|
||||||
" {} Failed to delete {}: {}",
|
" {} Failed to delete {}: {}",
|
||||||
"ERR".red(),
|
"ERR".red(),
|
||||||
&node.id[..8],
|
node.id.get(..8).unwrap_or(&node.id),
|
||||||
e
|
e
|
||||||
);
|
);
|
||||||
errors += 1;
|
errors += 1;
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,11 @@ fn main() -> anyhow::Result<()> {
|
||||||
// Read and parse backup
|
// Read and parse backup
|
||||||
let backup_content = std::fs::read_to_string(&backup_path)?;
|
let backup_content = std::fs::read_to_string(&backup_path)?;
|
||||||
let wrapper: Vec<BackupWrapper> = serde_json::from_str(&backup_content)?;
|
let wrapper: Vec<BackupWrapper> = serde_json::from_str(&backup_content)?;
|
||||||
let recall_result: RecallResult = serde_json::from_str(&wrapper[0].text)?;
|
// Guard the index: an empty backup array would panic on wrapper[0].
|
||||||
|
let first = wrapper
|
||||||
|
.first()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("backup wrapper array is empty — nothing to restore"))?;
|
||||||
|
let recall_result: RecallResult = serde_json::from_str(&first.text)?;
|
||||||
let memories = recall_result.results;
|
let memories = recall_result.results;
|
||||||
|
|
||||||
println!("Found {} memories to restore", memories.len());
|
println!("Found {} memories to restore", memories.len());
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue