Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger-ui

tests/migrate_credential.rs

//! **The source credential never leaves the call it was typed for.**
//!
//! `util::tests::a_source_password_never_reaches_anything_the_page_renders`
//! proves the page's TEXT is clean by running a canary through every function
//! that turns the source into words. This test guards the other half — the
//! things a page does that produce no string a test can inspect: writing to
//! browser storage, logging, building a URL, or simply keeping the secret in
//! the form after the request has been built.
//!
//! It reads the page's own source, because that is where those acts are
//! visible. The class of leak is not hypothetical: an audit of this codebase
//! found `ConnectorAuth` deriving `Debug` and printing passwords and bearer
//! tokens in full.

const PAGE: &str = include_str!("../src/pages/migrate.rs");
const UTIL: &str = include_str!("../src/util.rs");

/// The page with `//` comments removed — a comment naming a hazard must not
/// read as the hazard itself.
fn code(src: &str) -> String {
    src.lines()
        .map(|l| match l.find("//") {
            Some(i) => &l[..i],
            None => l,
        })
        .collect::<Vec<_>>()
        .join("\n")
}

#[test]
fn the_migrate_page_never_writes_the_credential_anywhere_that_outlives_the_call() {
    let page = code(PAGE);
    for forbidden in [
        // Browser storage of any kind. The bearer token lives in sessionStorage
        // by a deliberate decision made in grpc.rs; a FOREIGN registry's
        // password is not that, and it gets no such decision.
        "session_storage",
        "local_storage",
        "set_item",
        "Storage",
        // Anything that prints.
        "console::",
        "log!",
        "println!",
        "web_sys::console",
    ] {
        assert!(
            !page.contains(forbidden),
            "src/pages/migrate.rs uses `{forbidden}` — the source password must never \
             be stored, printed or logged"
        );
    }
}

#[test]
fn the_password_is_never_formatted_into_a_string() {
    // A credential that reaches `format!` has reached something renderable: an
    // error message, a URL, a log line, a title. There is no legitimate reason
    // for the source password to be in one, so the whole class is refused.
    for (n, line) in code(PAGE).lines().enumerate() {
        let formats = line.contains("format!") || line.contains("push_str") || line.contains("href");
        assert!(
            !(formats && line.contains("password")),
            "src/pages/migrate.rs:{} builds a string from the password: {line}",
            n + 1
        );
    }
}

#[test]
fn the_form_forgets_the_password_the_moment_a_request_is_built() {
    let page = code(PAGE);
    // The signal is emptied …
    assert!(
        page.contains("forgotten(f)"),
        "the page must clear its own state with util::forgotten"
    );
    // … and so is the DOM input, which holds a copy the signal does not own.
    assert!(
        page.contains("el.set_value(\"\")"),
        "the password input element must be cleared too — a signal reset leaves \
         the typed value in the DOM"
    );
    // Both of the two calls that carry the credential do it.
    assert_eq!(
        page.matches("forget_password();").count(),
        2,
        "Discover and Start must each forget the password; nothing else may carry it"
    );
    // And the field is a password field that no autofill will repopulate.
    assert!(PAGE.contains("id=\"mig-pass\" type=\"password\" autocomplete=\"off\""));
    // The page must never mirror the password back into the input it came from:
    // a `prop:value` binding would re-render the secret into the DOM.
    for line in PAGE.lines().filter(|l| l.contains("mig-pass")) {
        assert!(
            !line.contains("prop:value"),
            "the password input must stay uncontrolled: {line}"
        );
    }
}

#[test]
fn the_source_form_cannot_be_debug_printed_into_a_leak() {
    // A derived `Debug` is how `ConnectorAuth` leaks today. `SourceForm`
    // implements it by hand so there is nothing to remember not to do.
    assert!(
        UTIL.contains("impl std::fmt::Debug for SourceForm"),
        "SourceForm must implement Debug by hand, redacted"
    );
    let derive_line = UTIL
        .lines()
        .zip(UTIL.lines().skip(1))
        .find(|(_, next)| next.trim_start().starts_with("pub struct SourceForm"))
        .map(|(derive, _)| derive.to_string())
        .expect("SourceForm must exist");
    assert!(
        !derive_line.contains("Debug"),
        "SourceForm must not DERIVE Debug: {derive_line}"
    );
}