Sanitizing secrets

edit

Recording files are usually committed, so anything secret in a request or a response ends up in the repository: authentication headers, cookies, API keys in the URL, tokens in the bodies. Sanitizers rewrite the recordings before they are saved:

var options = new HttpRecordingOptions
{
    Sanitizers =
    {
        new HeaderRemovalSanitizer("Authorization", "Cookie"),
        new UriQueryParameterSanitizer("api_key", "sig"),
    },
};
  • HeaderRemovalSanitizer removes the named headers, compared case-insensitively, from the request and the response. It does not touch the URI or the bodies.
  • UriQueryParameterSanitizer replaces the value of the named query string parameters with ***.
  • Credentials in the URI (https://user:password@host/) are always removed, without a sanitizer.

The sanitizers run in the order of the Sanitizers list. There are none by default.

Rewrite a body

Implement IHttpRecordingSanitizer to change anything else. Sanitize receives the HttpRecordingEntry and changes it in place; RequestBody and ResponseBody are the bodies as bytes. This sanitizer replaces a known secret, such as a token read from the test configuration, in the response bodies:

// Replaces a known secret in the response bodies. It runs before the entries are saved.
public sealed class ResponseBodySanitizer(string secret) : IHttpRecordingSanitizer
{
    public void Sanitize(HttpRecordingEntry entry)
    {
        if (entry.ResponseBody is { } body)
        {
            var text = Encoding.UTF8.GetString(body);
            entry.ResponseBody = Encoding.UTF8.GetBytes(text.Replace(secret, "***", StringComparison.Ordinal));
        }
    }
}
var options = new HttpRecordingOptions
{
    Sanitizers = { new ResponseBodySanitizer("s3cr3t-t0ken") },
};

A sanitizer changes the recording, not the response: while recording, the code under test receives the real response; when replaying, it receives the sanitized one. A test that replays must therefore not depend on a value a sanitizer removed.

Sanitizers must be deterministic

The sanitizers also run on each incoming request before its fingerprint is computed. A request whose api_key is masked in the recording is masked the same way when it is looked up, so it still matches. This only works when a sanitizer gives the same result for the same input: replace a value with a constant, never with a random or time-based one.