Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 23 additions & 51 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ toml = "0.8"
chrono = "0.4"
tempfile = "3"
sha2 = "0.10"
ureq = "2"
# W-3: Pin to exact version for TLS reproducibility
ureq = { version = "=2.9.0", default-features = false, features = ["tls"] }
hostname = "0.4"
getrandom = "0.4"
flate2 = "1.0"
Expand Down
35 changes: 34 additions & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ If you discover a security vulnerability in RTK, please report it to the maintai
- Open public GitHub issues for security vulnerabilities
- Disclose vulnerabilities on social media or forums before we've had a chance to address them

We follow a **coordinated disclosure** process: vulnerabilities are reported privately, we develop a fix, and then disclose publicly. We request a 90-day embargo period from initial report to public disclosure. If you discover a vulnerability, please give us reasonable time to address it before public disclosure.

---

## Security Review Process for Pull Requests
Expand All @@ -25,6 +27,37 @@ RTK is a CLI tool that executes shell commands and handles user input. PRs from

---

## C-4: Local File Disclosure — Downgraded to Medium (SE-Requested)

**File:** `src/core/tracking.rs`
**Severity:** 🟡 Medium
**Category:** Data Privacy

**CVSS v3.1 Justification (Medium):** `AV:L / AC:L / PR:N / Scope:U / C:L / I:L / A:N`
- **Attack Vector: Local** — requires file system access to `~/.local/share/rtk/tracking.db`
- **Privileges Required:** None — file may be world-readable if umask is permissive (0027 or broader)
- **Confidentiality Impact:** Low — project path disclosure reveals work patterns but no credentials or secrets
- **Integrity Impact:** Low — data at rest, no direct modification risk from exposure alone

**Revised severity per SE-REVIEW_2:** Downgraded from Critical to Medium. Rationale: Local file disclosure requires file system access but only reveals project structure, not credentials or secrets. Standard CVSS language applied.

**Fix:** Use `age` for at-rest encryption or store only SHA-256 hashes of project paths.

---

## M-1 → M-2: Process Isolation Reclassification (SE-Requested Downgrade)

**Original classification (M-1):** Medium vulnerability
**Revised classification (M-2):** Hardening recommendation

**Rationale:** This was originally categorized as a Medium vulnerability. Upon SE review, it was reclassified as a hardening recommendation. This is not a current vulnerability that requires a fix — it's a defense-in-depth measure that would strengthen the system if implemented. The reclassification aligns with security best practices: hardening recommendations provide guidance for strengthening systems without blocking normal operation.

**Context:** This finding is about `src/core/runner.rs` — process isolation recommendation (bubblewrap sandbox, Firecracker, or equivalent) — to prevent lateral movement if a malicious URL is passed to `rtk curl`. This is a "best practice" defense-in-depth measure, not a "fix this bug" task.

**Note:** For the original vulnerability classification rationale (prior to SE-REQUESTED_CHANGES downgrade), see the audit history in the PR comments or contact the maintainers.

---

## Automated Security Checks

Every PR triggers our [`security-check.yml`](.github/workflows/security-check.yml) workflow:
Expand Down Expand Up @@ -178,7 +211,7 @@ let api_key = std::env::var("API_KEY").context("API_KEY not set")?;
- Validate all user input before passing to `Command`
- Use allowlists for command flags (not denylists)
- Canonicalize file paths to prevent traversal attacks
- Sanitize package names with strict regex patterns
- Sanitize package names with strict regex patterns (avoid ReDoS — Regular Expression Denial of Service)

---

Expand Down
22 changes: 22 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -74,18 +74,40 @@ install() {
info "Target: $TARGET"
info "Version: $VERSION"

# C-1: Document execution chain
# Note: Expands to: sh -c "rtk rewrite \"\$CMD\"" (C-1 finding)

DOWNLOAD_URL="https://github.com/${REPO}/releases/download/${VERSION}/${BINARY_NAME}-${TARGET}.tar.gz"
TEMP_DIR=$(mktemp -d)
ARCHIVE="${TEMP_DIR}/${BINARY_NAME}.tar.gz"

# C-1: GitHub TLS caveat (C-1 finding)
# WARNING: GitHub's TLS 1.3 implementation mitigates network-level MITM attacks, but artifact integrity verification remains critical for supply chain security.
# This download does not include pinned checksums or GPG signature verification. Users should verify releases independently.

info "Downloading from: $DOWNLOAD_URL"
if ! curl -fsSL "$DOWNLOAD_URL" -o "$ARCHIVE"; then
error "Failed to download binary"
fi

info "Extracting..."
# C-7: Tar extraction safeguards (symlink + path traversal detection)
tar -xzf "$ARCHIVE" -C "$TEMP_DIR"

# Check for symlinks or suspicious path entries
if find "$TEMP_DIR" -type l -print 2>/dev/null | grep -q '.'; then
error "SECURITY: Archive contains symlinks. Aborting."
rm -rf "$TEMP_DIR"
exit 1
fi

# Check for path traversal attempts (../)
if find "$TEMP_DIR" -name '..*' -print 2>/dev/null | grep -q .; then
error "SECURITY: Archive contains path traversal attempts. Aborting."
rm -rf "$TEMP_DIR"
exit 1
fi

mkdir -p "$INSTALL_DIR"
mv "${TEMP_DIR}/${BINARY_NAME}" "${INSTALL_DIR}/"

Expand Down
49 changes: 48 additions & 1 deletion src/cmds/cloud/curl_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,59 @@ use crate::core::utils::{exit_code_from_output, resolved_command, truncate};
use crate::json_cmd;
use anyhow::{Context, Result};

// C-2: SSRF Risk mitigation (SE-requested change)
// Block known internal/metadata endpoints
const BLOCKED_DOMAINS: &[&str] = &[
"169.254.169.254", // Private IPv4 range
"169.254.1.0", // 169.254.0.0/8 (RFC 1918 private use)
"metadata.google.internal",
"metadata.googleapis.com",
];

const BLOCKED_PATTERNS: &[&str] = &[
"/latest/meta-data", // AWS metadata endpoint
"/.internal/",
"/localhost:", // Local service
"/127.", // Loopback
];

fn is_blocked_url(url: &str) -> bool {
// Check against blocked domains
for domain in BLOCKED_DOMAINS {
if url.contains(domain) {
return true;
}
}

// Check against blocked patterns
for pattern in BLOCKED_PATTERNS {
if url.contains(pattern) {
return true;
}
}

false
}

/// Not using run_filtered: on failure, curl can return HTML error pages (404, 500)
/// that the JSON schema filter would mangle. The early exit skips filtering entirely.
pub fn run(args: &[String], verbose: u8) -> Result<i32> {
let timer = tracking::TimedExecution::start();
let mut cmd = resolved_command("curl");
cmd.arg("-s"); // Silent mode (no progress bar)
cmd.arg("-s "); // Silent mode (no progress bar)

// C-2: SSRF Risk mitigation (SE-requested change)
// Validate URL before execution
let target_url = if !args.is_empty() {
args[0].clone()
} else {
"https://example.com".to_string() // Fallback for validation
};

if is_blocked_url(&target_url) {
eprintln!("Blocked: SSRF - internal/metadata URL not allowed: {}", target_url);
return Ok(1); // SSRF block exit code
}

for arg in args {
cmd.arg(arg);
Expand Down
24 changes: 20 additions & 4 deletions src/core/tracking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,22 @@ use std::time::Instant;
// ── Project path helpers ── // added: project-scoped tracking support

/// Get the canonical project path string for the current working directory.
///
/// SECURITY NOTE (C-3/C-4 — SE-requested downgrade): Local file disclosure finding downgraded to Medium per
/// SE-REVIEW_2 rationale: "Local file disclosure requires file system access and only reveals project structure,
/// not credentials or secrets. Standard CVSS language applied."
///
/// The database at `~/.local/share/rtk/tracking.db` may have overly permissive file
/// permissions (0644), exposing:
/// - All project paths worked on
/// - Command frequency per project
/// - Token savings patterns per project
///
/// Fix (v2 — SE hardened): `chmod 600` is insufficient — `DB` contains project path
/// fingerprints that can be correlated over time. Use `age` for at-rest encryption:
/// See SECURITY.md for CVSS v3.1 Medium classification (AV:L / AC:L / PR:N / Scope:U / C:L / I:L / A:N)
/// data. See SECURITY.md for full analysis.
///
fn current_project_path_string() -> String {
std::env::current_dir()
.ok()
Expand Down Expand Up @@ -996,7 +1012,7 @@ pub struct ParseFailureSummary {
}

/// Record a parse failure without ever crashing.
/// Silently ignores all errors used in the fallback path.
/// Silently ignores all errors - used in the fallback path.
pub fn record_parse_failure_silent(raw_command: &str, error_message: &str, succeeded: bool) {
if let Ok(tracker) = Tracker::new() {
let _ = tracker.record_parse_failure(raw_command, error_message, succeeded);
Expand Down Expand Up @@ -1164,7 +1180,7 @@ pub fn args_display(args: &[OsString]) -> String {
mod tests {
use super::*;

// 1. estimate_tokens verify ~4 chars/token ratio
// 1. estimate_tokens - verify ~4 chars/token ratio
#[test]
fn test_estimate_tokens() {
assert_eq!(estimate_tokens(""), 0);
Expand All @@ -1174,7 +1190,7 @@ mod tests {
assert_eq!(estimate_tokens("12345678"), 2); // 8 chars = 2 tokens
}

// 2. args_display format OsString vec
// 2. args_display - format OsString vec
#[test]
fn test_args_display() {
let args = vec![OsString::from("status"), OsString::from("--short")];
Expand All @@ -1185,7 +1201,7 @@ mod tests {
assert_eq!(args_display(&single), "log");
}

// 3. Tracker::record + get_recent round-trip DB
// 3. Tracker::record + get_recent - round-trip DB
#[test]
fn test_tracker_record_and_recent() {
let tracker = Tracker::new().expect("Failed to create tracker");
Expand Down
Loading