Усилить проверку источника и ограничение запросов
This commit is contained in:
@@ -15,6 +15,7 @@ crank-registry = { path = "../crank-registry" }
|
||||
crank-runtime = { path = "../crank-runtime" }
|
||||
crank-schema = { path = "../crank-schema" }
|
||||
futures-util = "0.3"
|
||||
reqwest.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
sha2.workspace = true
|
||||
|
||||
@@ -12,6 +12,7 @@ use axum::{
|
||||
},
|
||||
};
|
||||
use futures_util::stream;
|
||||
use reqwest::Url;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::jsonrpc::{
|
||||
@@ -42,21 +43,44 @@ impl AllowedOrigins {
|
||||
}
|
||||
|
||||
pub(super) fn is_allowed(&self, origin: &str) -> bool {
|
||||
if origin.starts_with("http://localhost")
|
||||
|| origin.starts_with("http://127.0.0.1")
|
||||
|| origin.starts_with("https://localhost")
|
||||
|| origin.starts_with("https://127.0.0.1")
|
||||
{
|
||||
let Some(origin) = parse_origin(origin, true) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
if is_loopback_origin(&origin) {
|
||||
return true;
|
||||
}
|
||||
|
||||
match &self.public_origin {
|
||||
Some(public_origin) => public_origin == origin,
|
||||
Some(public_origin) => public_origin == &origin.origin().ascii_serialization(),
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_loopback_origin(origin: &Url) -> bool {
|
||||
matches!(origin.host_str(), Some("localhost" | "127.0.0.1" | "[::1]"))
|
||||
}
|
||||
|
||||
fn parse_origin(value: &str, origin_only: bool) -> Option<Url> {
|
||||
let origin = Url::parse(value).ok()?;
|
||||
if !matches!(origin.scheme(), "http" | "https")
|
||||
|| origin.host_str().is_none()
|
||||
|| !origin.username().is_empty()
|
||||
|| origin.password().is_some()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
if origin_only
|
||||
&& (origin.path() != "/" || origin.query().is_some() || origin.fragment().is_some())
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(origin)
|
||||
}
|
||||
|
||||
pub(super) fn validate_origin(
|
||||
allowed_origins: &AllowedOrigins,
|
||||
headers: &HeaderMap,
|
||||
@@ -298,14 +322,58 @@ pub(super) fn is_valid_request_id(value: &str) -> bool {
|
||||
}
|
||||
|
||||
pub(super) fn extract_origin(url: &str) -> Option<String> {
|
||||
let mut parts = url.split('/');
|
||||
let scheme = parts.next()?;
|
||||
let empty = parts.next()?;
|
||||
let authority = parts.next()?;
|
||||
Some(parse_origin(url, false)?.origin().ascii_serialization())
|
||||
}
|
||||
|
||||
if empty.is_empty() {
|
||||
return Some(format!("{scheme}//{authority}"));
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::AllowedOrigins;
|
||||
|
||||
#[test]
|
||||
fn allows_loopback_origins_with_and_without_port() {
|
||||
let origins = AllowedOrigins::new(None);
|
||||
|
||||
assert!(origins.is_allowed("http://localhost"));
|
||||
assert!(origins.is_allowed("http://localhost:3000"));
|
||||
assert!(origins.is_allowed("https://127.0.0.1:8443"));
|
||||
assert!(origins.is_allowed("http://[::1]:3000"));
|
||||
}
|
||||
|
||||
None
|
||||
#[test]
|
||||
fn rejects_hosts_that_only_prefix_match_loopback() {
|
||||
let origins = AllowedOrigins::new(None);
|
||||
|
||||
assert!(!origins.is_allowed("http://localhost.attacker.com"));
|
||||
assert!(!origins.is_allowed("http://127.0.0.1.attacker.com"));
|
||||
assert!(!origins.is_allowed("http://localhost@attacker.com"));
|
||||
assert!(!origins.is_allowed("http://attacker.com/http://localhost"));
|
||||
assert!(!origins.is_allowed("http://[::1].attacker.com"));
|
||||
assert!(!origins.is_allowed("http://attacker@[::1]"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matches_configured_public_origin_exactly() {
|
||||
let origins = AllowedOrigins::new(Some("https://crank.example.com".to_owned()));
|
||||
|
||||
assert!(origins.is_allowed("https://crank.example.com"));
|
||||
assert!(!origins.is_allowed("https://crank.example.com.attacker.com"));
|
||||
assert!(!origins.is_allowed("https://evil.example.com"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_non_http_schemes() {
|
||||
let origins = AllowedOrigins::new(None);
|
||||
|
||||
assert!(!origins.is_allowed("file://localhost"));
|
||||
assert!(!origins.is_allowed("javascript://localhost"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_values_that_are_not_origins() {
|
||||
let origins = AllowedOrigins::new(None);
|
||||
|
||||
assert!(!origins.is_allowed("http://localhost/path"));
|
||||
assert!(!origins.is_allowed("http://localhost?query=value"));
|
||||
assert!(!origins.is_allowed("http://localhost#fragment"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user