harden network and auth guards, boost themed fill
This commit is contained in:
@@ -540,8 +540,8 @@ async fn main() {
|
||||
.allow_methods([axum::http::Method::GET, axum::http::Method::POST, axum::http::Method::PUT, axum::http::Method::DELETE])
|
||||
.allow_headers([axum::http::header::CONTENT_TYPE, axum::http::header::HeaderName::from_static("x-admin-session")])
|
||||
} else {
|
||||
tracing::warn!("CORS_ORIGIN not set - using permissive CORS (set CORS_ORIGIN for production)");
|
||||
CorsLayer::permissive()
|
||||
tracing::warn!("CORS_ORIGIN not set - allowing same-origin requests only (set CORS_ORIGIN to allow cross-origin)");
|
||||
CorsLayer::new()
|
||||
};
|
||||
|
||||
let app = app
|
||||
|
||||
@@ -961,7 +961,7 @@ pub async fn create_room(
|
||||
RoomMode::Party => 20,
|
||||
};
|
||||
|
||||
let (tx, _) = broadcast::channel(256);
|
||||
let (tx, _) = broadcast::channel(1024);
|
||||
let grid = vec![vec![String::new(); width]; height];
|
||||
|
||||
let party_sub_str = req.get("party_sub").and_then(|v| v.as_str()).unwrap_or("wrong");
|
||||
@@ -1374,6 +1374,31 @@ async fn handle_ws(
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(broadcast::error::RecvError::Lagged(_)) => {
|
||||
// A slow consumer fell behind: re-send full state so the
|
||||
// client resyncs instead of being silently dropped or
|
||||
// disconnected.
|
||||
let rooms = state.rooms.read().await;
|
||||
if let Some(room) = rooms.get(&code) {
|
||||
let resync = serde_json::to_string(&WsMessage::FullState {
|
||||
grid: room.grid.clone(),
|
||||
players: room.players.values().map(|p| PlayerInfo {
|
||||
id: p.id.clone(),
|
||||
name: p.name.clone(),
|
||||
color: p.color.clone(),
|
||||
cursor: p.cursor,
|
||||
direction: p.direction.clone(),
|
||||
order: p.order,
|
||||
}).collect(),
|
||||
current_turn: room.current_player_id().map(|s| s.to_string()),
|
||||
started: room.started,
|
||||
your_id: token.clone(),
|
||||
}).unwrap();
|
||||
if send_text(&mut socket, resync).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ pub fn is_internal_url(url: &str) -> bool {
|
||||
if host.contains('@') { return true; }
|
||||
|
||||
// loopback
|
||||
host == "localhost"
|
||||
let blocked = host == "localhost"
|
||||
|| host.starts_with("127.") // full 127.0.0.0/8 range
|
||||
|| host == "::1"
|
||||
|| host.starts_with("::ffff:127.") // IPv4-mapped IPv6 loopback
|
||||
@@ -146,7 +146,8 @@ pub fn is_internal_url(url: &str) -> bool {
|
||||
// cloud metadata endpoints
|
||||
|| host == "169.254.169.254"
|
||||
|| host == "metadata.google.internal"
|
||||
|| host == "100.100.100.200" // Alibaba cloud metadata
|
||||
|| host == "100.100.100.200"; // Alibaba cloud metadata
|
||||
blocked || resolves_to_internal_ip(host)
|
||||
}
|
||||
|
||||
fn is_172_private(host: &str) -> bool {
|
||||
@@ -163,6 +164,47 @@ fn is_cgnat(host: &str) -> bool {
|
||||
(64..=127).contains(&second)
|
||||
}
|
||||
|
||||
fn is_internal_ip(ip: &std::net::IpAddr) -> bool {
|
||||
match ip {
|
||||
std::net::IpAddr::V4(v4) => {
|
||||
let o = v4.octets();
|
||||
o[0] == 0
|
||||
|| o[0] == 10
|
||||
|| o[0] == 127
|
||||
|| (o[0] == 169 && o[1] == 254)
|
||||
|| (o[0] == 172 && (16..=31).contains(&o[1]))
|
||||
|| (o[0] == 192 && o[1] == 168)
|
||||
|| (o[0] == 100 && (64..=127).contains(&o[1]))
|
||||
|| (o[0] == 198 && (o[1] == 18 || o[1] == 19))
|
||||
|| o[0] >= 224
|
||||
}
|
||||
std::net::IpAddr::V6(v6) => {
|
||||
v6.is_loopback()
|
||||
|| v6.is_unspecified()
|
||||
|| v6.is_multicast()
|
||||
|| (v6.segments()[0] & 0xfe00 == 0xfc00)
|
||||
|| (v6.segments()[0] & 0xffc0 == 0xfe80)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolves_to_internal_ip(host: &str) -> bool {
|
||||
let host = host.strip_prefix('[').and_then(|h| h.strip_suffix(']')).unwrap_or(host);
|
||||
if host.is_empty() || host.parse::<std::net::IpAddr>().is_ok() {
|
||||
return false;
|
||||
}
|
||||
use std::net::ToSocketAddrs;
|
||||
let Ok(addrs) = (host, 0_u16).to_socket_addrs() else {
|
||||
return false;
|
||||
};
|
||||
for addr in addrs {
|
||||
if is_internal_ip(&addr.ip()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn sanitize_html(input: &str) -> String {
|
||||
input
|
||||
.replace('&', "&")
|
||||
|
||||
@@ -131,7 +131,7 @@ pub fn create_registration_challenge(
|
||||
authenticator_attachment: None,
|
||||
resident_key: "preferred".into(),
|
||||
require_resident_key: false,
|
||||
user_verification: "preferred".into(),
|
||||
user_verification: "required".into(),
|
||||
},
|
||||
attestation: "none".into(),
|
||||
}
|
||||
@@ -204,6 +204,10 @@ pub fn verify_registration(
|
||||
if flags & 0x01 == 0 {
|
||||
return Err("user not present".into());
|
||||
}
|
||||
// UV flag (bit 2): user verification (PIN/biometric) is required
|
||||
if flags & 0x04 == 0 {
|
||||
return Err("user verification required".into());
|
||||
}
|
||||
if flags & 0x40 == 0 {
|
||||
return Err("no attested credential data".into());
|
||||
}
|
||||
@@ -301,6 +305,9 @@ pub fn verify_authentication(
|
||||
if auth_data[32] & 0x01 == 0 {
|
||||
return Err("user not present".into());
|
||||
}
|
||||
if auth_data[32] & 0x04 == 0 {
|
||||
return Err("user verification required".into());
|
||||
}
|
||||
|
||||
let counter = u32::from_be_bytes([auth_data[33], auth_data[34], auth_data[35], auth_data[36]]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user