fix: security hardening — SameSite=Strict, Secure cookie flag, password min length, generic registration error, auth rate limiting
This commit is contained in:
@@ -7,6 +7,12 @@ pub async fn execute(ctx: &AppContext, cmd: RegisterCommand) -> Result<(), Domai
|
||||
return Err(DomainError::Unauthorized("Registration is disabled".into()));
|
||||
}
|
||||
|
||||
if cmd.password.len() < 8 {
|
||||
return Err(DomainError::ValidationError(
|
||||
"Password must be at least 8 characters".into(),
|
||||
));
|
||||
}
|
||||
|
||||
let email = Email::new(cmd.email)?;
|
||||
|
||||
if ctx.user_repository.find_by_email(&email).await?.is_some() {
|
||||
|
||||
@@ -53,10 +53,14 @@ pub mod html {
|
||||
.replace('"', "%22")
|
||||
}
|
||||
|
||||
fn secure_flag() -> &'static str {
|
||||
if std::env::var("SECURE_COOKIES").as_deref() == Ok("true") { "; Secure" } else { "" }
|
||||
}
|
||||
|
||||
fn set_cookie_header(token: &str, max_age: i64) -> (axum::http::HeaderName, HeaderValue) {
|
||||
let val = format!(
|
||||
"token={}; HttpOnly; Path=/; SameSite=Lax; Max-Age={}",
|
||||
token, max_age
|
||||
"token={}; HttpOnly; Path=/; SameSite=Strict; Max-Age={}{}",
|
||||
token, max_age, secure_flag()
|
||||
);
|
||||
(SET_COOKIE, HeaderValue::from_str(&val).expect("valid cookie"))
|
||||
}
|
||||
@@ -104,10 +108,8 @@ pub mod html {
|
||||
}
|
||||
|
||||
pub async fn get_logout() -> impl IntoResponse {
|
||||
let cookie = (
|
||||
SET_COOKIE,
|
||||
HeaderValue::from_static("token=; HttpOnly; Path=/; SameSite=Lax; Max-Age=0"),
|
||||
);
|
||||
let val = format!("token=; HttpOnly; Path=/; SameSite=Strict; Max-Age=0{}", secure_flag());
|
||||
let cookie = (SET_COOKIE, HeaderValue::from_str(&val).expect("valid cookie"));
|
||||
([cookie], Redirect::to("/")).into_response()
|
||||
}
|
||||
|
||||
@@ -162,9 +164,8 @@ pub mod html {
|
||||
Err(_) => Redirect::to("/login").into_response(),
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let msg = encode_error(&e.to_string());
|
||||
Redirect::to(&format!("/register?error={}", msg)).into_response()
|
||||
Err(_) => {
|
||||
Redirect::to("/register?error=Registration+failed.+Please+try+again.").into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,49 @@
|
||||
use axum::{Router, routing};
|
||||
use std::sync::{
|
||||
Arc,
|
||||
atomic::{AtomicU64, Ordering},
|
||||
};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use axum::{Router, http::StatusCode, middleware, response::IntoResponse, routing};
|
||||
use tower_http::{services::ServeDir, trace::TraceLayer};
|
||||
|
||||
use crate::{handlers, state::AppState};
|
||||
|
||||
/// Simple global rate limiter: tracks request count per 60-second window.
|
||||
/// Not per-IP — suitable for a low-traffic personal app.
|
||||
#[derive(Clone)]
|
||||
struct RateLimiter {
|
||||
window: Arc<AtomicU64>,
|
||||
count: Arc<AtomicU64>,
|
||||
limit: u64,
|
||||
}
|
||||
|
||||
impl RateLimiter {
|
||||
fn new(limit: u64) -> Self {
|
||||
Self {
|
||||
window: Arc::new(AtomicU64::new(0)),
|
||||
count: Arc::new(AtomicU64::new(0)),
|
||||
limit,
|
||||
}
|
||||
}
|
||||
|
||||
fn check(&self) -> bool {
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs()
|
||||
/ 60;
|
||||
let prev = self.window.load(Ordering::Relaxed);
|
||||
if now != prev {
|
||||
self.window.store(now, Ordering::Relaxed);
|
||||
self.count.store(1, Ordering::Relaxed);
|
||||
true
|
||||
} else {
|
||||
self.count.fetch_add(1, Ordering::Relaxed) + 1 <= self.limit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_router(state: AppState) -> Router {
|
||||
Router::new()
|
||||
.merge(html_routes())
|
||||
@@ -13,10 +54,9 @@ pub fn build_router(state: AppState) -> Router {
|
||||
}
|
||||
|
||||
fn html_routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/", routing::get(handlers::html::get_activity_feed))
|
||||
.route("/users", routing::get(handlers::html::get_users_list))
|
||||
.route("/users/{id}", routing::get(handlers::html::get_user_profile))
|
||||
// Auth routes: 20 requests per minute globally.
|
||||
let limiter = RateLimiter::new(20);
|
||||
let auth = Router::new()
|
||||
.route(
|
||||
"/login",
|
||||
routing::get(handlers::html::get_login_page)
|
||||
@@ -28,6 +68,22 @@ fn html_routes() -> Router<AppState> {
|
||||
routing::get(handlers::html::get_register_page)
|
||||
.post(handlers::html::post_register),
|
||||
)
|
||||
.route_layer(middleware::from_fn(move |req: axum::extract::Request, next: middleware::Next| {
|
||||
let limiter = limiter.clone();
|
||||
async move {
|
||||
if limiter.check() {
|
||||
next.run(req).await
|
||||
} else {
|
||||
StatusCode::TOO_MANY_REQUESTS.into_response()
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
Router::new()
|
||||
.route("/", routing::get(handlers::html::get_activity_feed))
|
||||
.route("/users", routing::get(handlers::html::get_users_list))
|
||||
.route("/users/{id}", routing::get(handlers::html::get_user_profile))
|
||||
.merge(auth)
|
||||
.route("/reviews/new", routing::get(handlers::html::get_new_review_page))
|
||||
.route("/reviews", routing::post(handlers::html::post_review))
|
||||
.route("/reviews/{id}/delete", routing::post(handlers::html::post_delete_review))
|
||||
@@ -37,6 +93,18 @@ fn html_routes() -> Router<AppState> {
|
||||
}
|
||||
|
||||
fn api_routes() -> Router<AppState> {
|
||||
let limiter = RateLimiter::new(20);
|
||||
let auth_rate_limit = middleware::from_fn(move |req: axum::extract::Request, next: middleware::Next| {
|
||||
let limiter = limiter.clone();
|
||||
async move {
|
||||
if limiter.check() {
|
||||
next.run(req).await
|
||||
} else {
|
||||
StatusCode::TOO_MANY_REQUESTS.into_response()
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Router::new().nest(
|
||||
"/api",
|
||||
Router::new()
|
||||
@@ -52,6 +120,7 @@ fn api_routes() -> Router<AppState> {
|
||||
routing::post(handlers::api::sync_poster),
|
||||
)
|
||||
.route("/auth/login", routing::post(handlers::api::login))
|
||||
.route("/auth/register", routing::post(handlers::api::register)),
|
||||
.route("/auth/register", routing::post(handlers::api::register))
|
||||
.route_layer(auth_rate_limit),
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user