Secure Error Messages: Prevent Sensitive Data Leaks
Hey folks! Let's talk about something super important for keeping our systems secure: error messages. Sounds kinda boring, right? But trust me, it's crucial! When our code goes sideways, the error messages it spits out can reveal way more than we want, like juicy tidbits of information that attackers can use to their advantage. We're talking email addresses, internal file paths, API endpoints – the whole shebang! This is where we will discuss how to improve our system to prevent sensitive data from leaking through error messages. So, let's dive in and make sure our error messages are helping us, not hurting us!
🚨 The Current Situation: Information Leakage
Right now, the keyless signing implementation, which is super important for, well, signing things without needing to manage secret keys directly, has a bit of a problem. Its error messages are like open books, spilling sensitive data all over the place. This is a potential risk that the security level is MEDIUM, so we need to fix it ASAP, especially considering things like GDPR and CCPA. Attackers could use this information to launch targeted attacks, get access to systems, and do all sorts of nasty stuff. It's like leaving your front door unlocked – not a good look!
Here's a breakdown of what's currently going wrong:
- Email Addresses Exposed: Imagine error messages straight-up revealing user emails. That's a privacy nightmare and a goldmine for phishing attacks.
 - Token Fragments & Full Tokens Leaked: Error messages are sometimes logging full tokens or parts of them. If an attacker gets ahold of a token, they can impersonate a user. Bye-bye, security!
 - Internal File Paths Revealed: Full paths to files and directories are being shown. This gives attackers a roadmap of your system and helps them figure out where to look for more vulnerabilities.
 - API Endpoints & Internal URLs Exposed: Leaking internal URLs is like handing out the keys to your internal systems. It allows attackers to understand the internal structure of your system.
 - Certificate Details Disclosed: Revealing certificate details can assist attackers in performing man-in-the-middle attacks, which can lead to data breaches.
 
Why This Matters
This kind of information disclosure isn't just a minor inconvenience; it can have serious consequences:
- Revealing Personally Identifiable Information (PII): Exposing emails, user IDs, and other sensitive data is a direct violation of privacy regulations.
 - Exposing System Internals: Giving attackers a peek behind the curtain of your system architecture allows them to plan and execute targeted attacks more effectively.
 - Aiding Reconnaissance: It's like providing the enemy with a map of your defenses. Attackers use this information to understand your system and plan their attacks.
 - Violating Privacy Regulations: Compliance with GDPR, CCPA, and other privacy regulations is crucial. Data leaks can lead to hefty fines and reputational damage.
 - Leaking Authentication Tokens: If attackers can snag a token, they can impersonate users and gain access to protected resources.
 
In essence, it gives bad guys a massive advantage, which can lead to social engineering or targeted attacks. It's time to batten down the hatches and protect our information.
🛠️ Fixing the Problem: The Implementation Plan
Okay, so we know what's wrong. Now, how do we fix it? We need to sanitize and redact the error messages. This means removing or obscuring sensitive data while still providing useful information to developers for debugging purposes. Here’s a detailed plan, with code examples, to show you how we're going to tackle this.
Step 1: Crafting an Error Sanitization Helper
First things first, we need a way to sanitize the error messages. We will create a trait called SanitizeError that will be implemented for strings. The implementation will include regular expressions to find and redact sensitive information.
pub trait SanitizeError {
    fn sanitize(&self) -> String;
}
impl SanitizeError for String {
    fn sanitize(&self) -> String {
        // Redact emails
        let email_regex = Regex::new(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}").unwrap();
        let sanitized = email_regex.replace_all(self, "<email-redacted>");
        
        // Redact tokens (base64 patterns > 20 chars)
        let token_regex = Regex::new(r"[A-Za-z0-9+/]{20,}={0,2}").unwrap();
        let sanitized = token_regex.replace_all(&sanitized, "<token-redacted>");
        
        // Redact absolute paths
        let path_regex = Regex::new(r"/[\w/.-]+").unwrap();
        let sanitized = path_regex.replace_all(&sanitized, "<path-redacted>");
        
        sanitized.to_string()
    }
}
In this code:
- We define a trait 
SanitizeErrorwith asanitizefunction. - We implement this trait for 
String. - Inside the 
sanitizefunction, we use regular expressions to find and replace email addresses, tokens, and absolute paths with redacted placeholders. 
This is the core of our sanitization process, ensuring that sensitive data isn't directly exposed in our error messages.
Step 2: Implementing Dual-Level Error Logging
Next up, we need to implement a logging strategy that caters to both developers and users. This involves logging full error details for debugging (for internal use) and sanitized errors for production logs (for external use).
pub fn log_error_safely<E: std::fmt::Display>(context: &str, error: E) {
    // Full details at debug level (for developers)
    log::debug!("[{}] Full error: {}", context, error);
    
    // Sanitized error at error level (for users/production logs)
    log::error!("[{}] {}", context, error.to_string().sanitize());
}
- We've created a function 
log_error_safelythat takes an error and a context string. - It logs the full error to the debug level, which is useful for developers and debugging purposes.
 - It then logs a sanitized version of the error to the error level, which is what users and production logs will see.
 
This two-pronged approach ensures that developers have enough information for debugging while preventing sensitive information from being exposed in production logs.
Step 3: Redacting Emails
Emails are super sensitive, so we need a dedicated function to redact them properly.
pub fn redact_email(email: &str) -> String {
    if let Some(at_pos) = email.find('@') {
        let local = &email[..at_pos];
        let domain = &email[at_pos + 1..];
        
        if local.len() <= 2 {
            format!("{}@{}", "*".repeat(local.len()), domain)
        } else {
            format!("{}***@{}", &local[..2], domain)
        }
    } else {
        "<invalid-email>".to_string()
    }
}
- The 
redact_emailfunction takes an email address as input. - It finds the 
@symbol in the email. - It redacts the local part of the email (the part before the 
@) by either replacing it with asterisks or using the first two characters and then asterisks, keeping the domain visible. - If the email is invalid, it returns 
<invalid-email>. For example:alice.smith@example.combecomesal***@example.com 
Step 4: Sanitizing Paths
Lastly, we'll sanitize file paths to prevent leaking internal file structure information.
pub fn sanitize_path(path: &Path) -> String {
    if let Some(home) = dirs::home_dir() {
        if let Ok(relative) = path.strip_prefix(&home) {
            return format!("~/{}", relative.display());
        }
    }
    
    // Fallback: use filename only
    path.file_name()
        .map(|f| f.to_string_lossy().to_string())
        .unwrap_or_else(|| "<file>".to_string())
}
- The 
sanitize_pathfunction takes aPathas input. - It first checks if the path is within the user's home directory.
 - If it is, it replaces the home directory with 
~(tilde) for brevity. - If it's not in the home directory, it returns only the file name or 
<file>if no filename is available. Example:/home/user/.sigstore/tokenbecomes~/.sigstore/token. 
Implementation Summary
By following these steps, we make sure that our error messages are informative for developers, but also safe for users. This will require some effort, but it will improve our security posture a lot.
✅ Audit Checklist
Here's a checklist to make sure we've covered all our bases:
src/lib/src/signature/keyless/oidc.rs- Token parsing errorssrc/lib/src/signature/keyless/fulcio.rs- Certificate errorssrc/lib/src/signature/keyless/rekor.rs- Log upload errorssrc/lib/src/signature/keyless/format.rs- Parsing errorssrc/lib/src/signature/keyless/signer.rs- Signing errors- All 
log::error!()andlog::warn()statements - All 
ErrortypeDisplayimplementations - All 
anyhow::Contextmessages 
This checklist will ensure that all error messages are reviewed and sanitized as needed.
🧪 Testing Time!
To ensure our new code works, we need to create some tests.
#[test]
fn test_email_redaction() {
    assert_eq!(
        redact_email("alice.smith@example.com"),
        "al***@example.com"
    );
    
    assert_eq!(
        redact_email("a@test.com"),
        "*@test.com"
    );
}
#[test]
fn test_token_sanitization() {
    let error_msg = "Failed to parse token: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...";
    let sanitized = error_msg.sanitize();
    
    assert!(!sanitized.contains("eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9"));
    assert!(sanitized.contains("<token-redacted>"));
}
#[test]
fn test_path_sanitization() {
    let path = PathBuf::from("/home/user/.sigstore/token");
    let sanitized = sanitize_path(&path);
    
    assert_eq!(sanitized, "~/.sigstore/token");
}
- We create tests for the 
redact_emailfunction to make sure that it correctly redacts email addresses. - We test the 
sanitizemethod to ensure it successfully redacts tokens. - We create tests for the 
sanitize_pathto verify it correctly sanitizes the paths. 
⚙️ Configuration
We might want to configure this behavior, which can be done like this:
pub struct LoggingConfig {
    /// Enable PII redaction in logs (default: true)
    pub redact_pii: bool,
    
    /// Allow full error details in debug builds (default: true)
    pub debug_full_errors: bool,
}
💡 Common Patterns to Avoid
Here are some examples of what NOT to do, along with the right way to do it. It's like a style guide for error messages.
| ❌ BAD | ✅ GOOD | 
|---|---|
| `format!( |