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}"
);
}