gitbutlerapp/gitbutler · error

BUG: Sensitive data cannot be serialized - it needs to be ex

Error message

BUG: Sensitive data cannot be serialized - it needs to be extracted and put into a struct for serialization explicitly

What it means

`Sensitive<T>` in but-secret is a secrecy wrapper whose blanket serde `Serialize` impl deliberately panics: serializing secrets accidentally (state files, logs, IPC payloads, API DTOs) is treated as a bug. To persist or transmit data containing a `Sensitive` field you must build an explicit serializable struct and consciously include (or omit) the secret (crates/but-secret/src/sensitive.rs:15).

Source

Thrown at crates/but-secret/src/sensitive.rs:15

use std::ops::{Deref, DerefMut};

use serde::{Deserialize, Deserializer, Serialize, Serializer};

use crate::Sensitive;

impl<T> Serialize for Sensitive<T>
where
    T: Serialize,
{
    fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        unreachable!(
            "BUG: Sensitive data cannot be serialized - it needs to be extracted and put into a struct for serialization explicitly"
        )
    }
}
impl<'de, T> Deserialize<'de> for Sensitive<T>
where
    T: Deserialize<'de>,
{
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        T::deserialize(deserializer).map(Sensitive)
    }
}

impl<T> std::fmt::Debug for Sensitive<T>
where

View on GitHub (pinned to caf1f223d3)

Solutions

  1. Introduce a dedicated serializable DTO that omits the secret or exposes it explicitly via the wrapper's expose API, and convert before serializing
  2. If the field must stay on the serialized type, exclude it with `#[serde(skip)]` and default-construct it on load
  3. Audit with a grep for `Sensitive<` inside `#[derive(Serialize)]` types as a review check

Example fix

// before
#[derive(serde::Serialize)]
struct Session {
    name: String,
    token: but_secret::Sensitive<String>, // panics at to_string()
}
serde_json::to_string(&session)?;

// after - explicit DTO, secret consciously handled
#[derive(serde::Serialize)]
struct SessionDto {
    name: String,
    // token intentionally omitted; never serialized
}
let dto = SessionDto { name: session.name.clone() };
serde_json::to_string(&dto)?;
Defensive patterns

Strategy: type-guard

Validate before calling

// Keep Sensitive fields out of serialized types by construction:
// define DTOs explicitly and convert before serde ever sees the domain type
#[derive(serde::Serialize)]
struct AuthDto {
    username: String,
    // no token field: Sensitive<String> stays in the domain type only
}

Type guard

// Compile-time guard: registering a domain type for serialization fails if it (still)
// contains Sensitive, because Sensitive's Serialize impl panics at runtime - so instead
// whitelist explicit DTOs:
trait SafeSerialize: serde::Serialize {}
impl SafeSerialize for AuthDto {} // only DTOs opt in; domain types never implement it

Try / catch

// If a panic escapes third-party code:
let json = std::panic::catch_unwind(|| serde_json::to_string(&value))
    .map_err(|_| anyhow::anyhow!("type contained Sensitive data; build an explicit DTO"))?;

Prevention

When it happens

Trigger: `#[derive(Serialize)]` on any struct that contains a `Sensitive<T>` field, followed by serde_json/toml serialization or returning it from a Tauri command; recursively serializing a domain type that embeds Sensitive.

Common situations: Adding a new config/state/session struct that carries a token or credential; passing internal domain types across an API boundary instead of a DTO; snapshotting or logging app state for debugging.

Related errors


AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20). Data as JSON: /api/errors/d56b2606e1e94d41. Report an issue: GitHub.