Detect leaked passwords

Detect requests that contain submitted passwords and use a service to determine whether the password has leaked before allowing the request to proceed to origin (data from haveibeenpwned).

VCL

Use this solution in your VCL service (click RUN below to test this solution or clone it to make changes):

Compute@Edge

Use this solution in your Compute@Edge service:

  1. Rust
  2. JavaScript
Cargo.toml
Rust
[dependencies]
fastly = "0.7.1"
sha1 = "0.6"
percent-encoding = "2.1.0"
login.html
Rust
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Compromised password detection demo</title>
</head>
<body>
<form action="/post" method="post">
<div class="container">
<label for="username"><b>Username</b></label>
<input type="text" placeholder="Enter Username" name="username" required />
<label for="password"><b>Password</b></label>
<input type="password" placeholder="Enter Password" name="password" required />
<button type="submit">Login</button>
</div>
</form>
</body>
</html>
main.rs
Rust
use fastly::http::{Method, StatusCode};
use fastly::{mime, Error, Request, Response};
use percent_encoding::percent_decode_str;
use sha1::Sha1;
// The name of the backend servers associated with this service.
// This must match the backend names you configured using `fastly backend create`.
const BACKEND_APP_SERVER: &str = "primary";
const BACKEND_SECURITY_CHECK: &str = "api";
// Credential prefix length
const PREFIX_LENGTH: usize = 5;
// Helper function to parse for password field
fn sub_field<'a>(content: &'a str, field_name: &str, separator_character: &str) -> Option<&'a str> {
content
.split(separator_character)
.find_map(|sub_field| field_value(sub_field, field_name))
}
fn field_value<'a>(content: &'a str, field_name: &str) -> Option<&'a str> {
let mut i = content.split('=');
let name = i.next()?.trim();
if name == field_name {
let value = i.next()?.trim();
Some(value)
} else {
None
}
}
/// Generate SHA1 hash from string s
fn hash_sha1(s: &str) -> String {
let mut hasher = Sha1::new();
hasher.update(s.as_bytes());
hasher.digest().to_string()
}
#[fastly::main]
fn main(mut req: Request) -> Result<Response, Error> {
// For the demo, serve a basic login form on the root path
if req.get_method() == Method::GET && req.get_path() == "/" {
let page_html = include_str!("login.html");
return Ok(Response::from_status(StatusCode::OK)
.with_content_type(mime::TEXT_HTML_UTF_8)
.with_body(page_html));
}
// Pass all requests through the credential detection, which
// modifies the request to enrich it with new information
req = process_credential(req)?;
// Send request to the primary origin as normal
Ok(req.send(BACKEND_APP_SERVER)?)
}
// Process login with threat check
fn process_credential(mut req: Request) -> Result<Request, Error> {
// Get body as a string from the request
let body_string = req.take_body_str();
// Get credential from body string
match sub_field(&body_string, "password", "&") {
Some("") | None => {
// No credential or empty credential found
println!("No valid credential is found");
req.set_header("fastly-password-status", "no-credential");
}
Some(plain_cred) => {
// Decode the percent-encoded password
let decoded_plain_cred = percent_decode_str(plain_cred).decode_utf8().unwrap();
// Generate sha1 hash of credential
let hashed_cred: String = hash_sha1(&decoded_plain_cred).to_uppercase();
// Split the hash of credential to left and right part at position PREFIX_LENGTH
let (hash_left, hash_right) = hashed_cred.split_at(PREFIX_LENGTH);
// Prepare the request for threat check
// (If you use HIBP in production please use an API key)
let api_url = format!("https://api.pwnedpasswords.com/range/{}", hash_left);
let api_req = Request::get(api_url);
// Send threat check request to API with the left-hand-side of the SHA1 hash
let mut api_resp = api_req.send(BACKEND_SECURITY_CHECK)?;
let api_resp_body = api_resp.take_body().into_string();
// Check if the response body contains the right-hand-side of the sha1 hash
let result = if api_resp_body.contains(hash_right) {
"compromised-credential"
} else {
"safe-credential"
};
req.set_header("fastly-password-status", result);
// Uncomment for debugging. For production use, avoid logging credentials
//println!("Checked credential {}, result is {}", plain_cred, result);
}
};
// Return the body string to the request before giving the request back to the main function
req.set_body(body_string);
Ok(req)
}