harden network and auth guards, boost themed fill

This commit is contained in:
2026-08-23 17:51:46 +03:00
parent aa70c61abd
commit 4dc4d615e5
5 changed files with 92 additions and 8 deletions
+12 -2
View File
@@ -241,6 +241,16 @@ pub fn generate_themed_puzzle(
) -> Result<Puzzle> {
let start = Instant::now();
// Bias the fill toward theme words when a theme is requested.
let theme_boost = theme_words
.filter(|w| !w.is_empty())
.map(|tw| {
let mut d = dict.clone();
d.boost_theme(tw);
d
});
let solve_dict = theme_boost.as_ref().unwrap_or(dict);
let cores = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1)
@@ -250,7 +260,7 @@ pub fn generate_themed_puzzle(
if cores > 1 && config.width > 9 {
return parallel_generate(
config,
dict,
solve_dict,
clue_db,
difficulty,
theme,
@@ -260,7 +270,7 @@ pub fn generate_themed_puzzle(
);
}
sequential_generate(config, dict, clue_db, difficulty, theme, theme_words, start)
sequential_generate(config, solve_dict, clue_db, difficulty, theme, theme_words, start)
}
struct SolvedGrid {
+2 -2
View File
@@ -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
+26 -1
View File
@@ -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,
}
}
+44 -2
View File
@@ -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('&', "&amp;")
+8 -1
View File
@@ -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]]);