mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-06 01:17:48 +00:00
fix(extension): harden local handoff boundaries
This commit is contained in:
+1
-1
Submodule Extensions/Browser updated: 8a6dca9692...a025c076e1
@@ -174,6 +174,10 @@ async fn ping_handler(
|
||||
headers: HeaderMap,
|
||||
body: Bytes,
|
||||
) -> Result<Response, StatusCode> {
|
||||
if !has_allowed_request_origin(&headers) {
|
||||
return Err(StatusCode::FORBIDDEN);
|
||||
}
|
||||
|
||||
let signature = match headers
|
||||
.get("x-firelink-signature")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
@@ -224,6 +228,10 @@ async fn download_handler(
|
||||
headers: HeaderMap,
|
||||
body: Bytes,
|
||||
) -> Result<StatusCode, StatusCode> {
|
||||
if !has_allowed_request_origin(&headers) || !has_valid_optional_nonce(&headers) {
|
||||
return Err(StatusCode::FORBIDDEN);
|
||||
}
|
||||
|
||||
let signature = match headers
|
||||
.get("x-firelink-signature")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
@@ -310,6 +318,15 @@ fn normalize_download(payload: ExtensionRequest) -> Option<ExtensionDownload> {
|
||||
if urls.is_empty() {
|
||||
return None;
|
||||
}
|
||||
if payload.media
|
||||
&& urls.iter().any(|url| {
|
||||
Url::parse(url)
|
||||
.ok()
|
||||
.is_none_or(|url| !matches!(url.scheme(), "http" | "https"))
|
||||
})
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let referer = payload.referer.and_then(|value| {
|
||||
let url = Url::parse(value.trim()).ok()?;
|
||||
@@ -405,6 +422,20 @@ fn is_valid_client_nonce(value: &str) -> bool {
|
||||
value.len() == 32 && value.bytes().all(|byte| byte.is_ascii_hexdigit())
|
||||
}
|
||||
|
||||
fn has_allowed_request_origin(headers: &HeaderMap) -> bool {
|
||||
match headers.get("origin") {
|
||||
None => true,
|
||||
Some(origin) => origin.to_str().ok().is_some_and(is_allowed_origin),
|
||||
}
|
||||
}
|
||||
|
||||
fn has_valid_optional_nonce(headers: &HeaderMap) -> bool {
|
||||
match headers.get(CLIENT_NONCE_HEADER) {
|
||||
None => true,
|
||||
Some(nonce) => nonce.to_str().ok().is_some_and(is_valid_client_nonce),
|
||||
}
|
||||
}
|
||||
|
||||
fn sign_server_proof(
|
||||
timestamp_text: &str,
|
||||
nonce: &str,
|
||||
@@ -491,13 +522,20 @@ fn is_allowed_origin(origin: &str) -> bool {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
add_server_identity, is_valid_client_nonce, normalize_download, sign_server_proof,
|
||||
ExtensionRequest, PROTOCOL_VERSION_HEADER, SERVER_HEADER,
|
||||
add_server_identity, claim_request, has_allowed_request_origin, has_valid_optional_nonce,
|
||||
is_valid_client_nonce, normalize_download, sign_server_proof, ExtensionRequest,
|
||||
PROTOCOL_VERSION_HEADER, SERVER_HEADER,
|
||||
};
|
||||
use axum::{
|
||||
http::{HeaderMap, HeaderValue, StatusCode},
|
||||
middleware,
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
use axum::{http::StatusCode, middleware, routing::get, Router};
|
||||
use hmac::{Hmac, KeyInit, Mac};
|
||||
use sha2::Sha256;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
|
||||
#[tokio::test]
|
||||
async fn identifies_every_extension_server_response() {
|
||||
@@ -534,6 +572,54 @@ mod tests {
|
||||
assert!(!is_valid_client_nonce("0123456789abcdef0123456789abcdeg"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_origins_and_malformed_optional_nonces() {
|
||||
let mut headers = HeaderMap::new();
|
||||
assert!(has_allowed_request_origin(&headers));
|
||||
assert!(has_valid_optional_nonce(&headers));
|
||||
|
||||
headers.insert(
|
||||
"origin",
|
||||
HeaderValue::from_static("https://not-firelink.example"),
|
||||
);
|
||||
assert!(!has_allowed_request_origin(&headers));
|
||||
|
||||
headers.insert(
|
||||
"origin",
|
||||
HeaderValue::from_static("moz-extension://firelink"),
|
||||
);
|
||||
headers.insert(
|
||||
"x-firelink-client-nonce",
|
||||
HeaderValue::from_static("not-a-valid-nonce"),
|
||||
);
|
||||
assert!(has_allowed_request_origin(&headers));
|
||||
assert!(!has_valid_optional_nonce(&headers));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn media_handoffs_reject_non_http_page_urls() {
|
||||
let download = normalize_download(ExtensionRequest {
|
||||
urls: vec!["ftp://example.com/audio.mp3".to_string()],
|
||||
referer: None,
|
||||
silent: false,
|
||||
filename: None,
|
||||
headers: None,
|
||||
cookies: None,
|
||||
media: true,
|
||||
});
|
||||
|
||||
assert!(download.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_replayed_download_signature() {
|
||||
let cache = Arc::new(Mutex::new(HashMap::new()));
|
||||
let signature = "a".repeat(64);
|
||||
|
||||
assert!(claim_request(&signature, 42, &cache));
|
||||
assert!(!claim_request(&signature, 42, &cache));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_media_drops_the_extension_cookie_header() {
|
||||
let download = normalize_download(ExtensionRequest {
|
||||
|
||||
Reference in New Issue
Block a user