Skip to content

Commit 73044e7

Browse files
lovasoacursoragent
andauthored
nice visual error messages for invalid header values
* Feat: Sanitize header values to prevent injection attacks Co-authored-by: contact <[email protected]> * Refactor sanitize_header_value to use Cow and remove test file Co-authored-by: contact <[email protected]> * refactor header error handling in render.rs --------- Co-authored-by: Cursor Agent <[email protected]>
1 parent 2e71702 commit 73044e7

File tree

2 files changed

+30
-15
lines changed

2 files changed

+30
-15
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# CHANGELOG.md
22

3+
## v0.39.1 (unreleased)
4+
- More precise server timing tracking to debug performance issues
5+
- Fix missing server timing header in some cases
6+
- Implement nice error messages for some header-related errors such as invalid header values.
7+
38
## v0.39.0 (2025-10-28)
49
- Ability to execute sql for URL paths with another extension. If you create sitemap.xml.sql, it will be executed for example.com/sitemap.xml
510
- Display source line info in errors even when the database does not return a precise error position. In this case, the entire problematic SQL statement is referenced.

src/render.rs

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,10 @@ use crate::webserver::http::RequestContext;
4646
use crate::webserver::response_writer::{AsyncResponseWriter, ResponseWriter};
4747
use crate::webserver::ErrorWithStatus;
4848
use crate::AppState;
49+
use actix_web::body::MessageBody;
4950
use actix_web::cookie::time::format_description::well_known::Rfc3339;
5051
use actix_web::cookie::time::OffsetDateTime;
52+
use actix_web::http::header::TryIntoHeaderPair;
5153
use actix_web::http::{header, StatusCode};
5254
use actix_web::{HttpResponse, HttpResponseBuilder};
5355
use anyhow::{bail, format_err, Context as AnyhowContext};
@@ -116,7 +118,7 @@ impl HeaderContext {
116118
Some(HeaderComponent::HttpHeader) => {
117119
self.add_http_header(&data).map(PageContext::Header)
118120
}
119-
Some(HeaderComponent::Redirect) => self.redirect(&data).map(PageContext::Close),
121+
Some(HeaderComponent::Redirect) => self.redirect(&data),
120122
Some(HeaderComponent::Json) => self.json(&data),
121123
Some(HeaderComponent::Csv) => self.csv(&data).await,
122124
Some(HeaderComponent::Cookie) => self.add_cookie(&data).map(PageContext::Header),
@@ -167,7 +169,9 @@ impl HeaderContext {
167169
self.response.status(StatusCode::FOUND);
168170
self.has_status = true;
169171
}
170-
self.response.insert_header((name.as_str(), value_str));
172+
let header = TryIntoHeaderPair::try_into_pair((name.as_str(), value_str))
173+
.map_err(|e| anyhow::anyhow!("Invalid header: {name}:{value_str}: {e:#?}"))?;
174+
self.response.insert_header(header);
171175
}
172176
Ok(self)
173177
}
@@ -237,13 +241,13 @@ impl HeaderContext {
237241
Ok(self)
238242
}
239243

240-
fn redirect(mut self, data: &JsonValue) -> anyhow::Result<HttpResponse> {
244+
fn redirect(mut self, data: &JsonValue) -> anyhow::Result<PageContext> {
241245
self.response.status(StatusCode::FOUND);
242246
self.has_status = true;
243247
let link = get_object_str(data, "link")
244248
.with_context(|| "The redirect component requires a 'link' property")?;
245249
self.response.insert_header((header::LOCATION, link));
246-
Ok(self.into_response_builder().body(()))
250+
self.close_with_body(())
247251
}
248252

249253
/// Answers to the HTTP request with a single json object
@@ -256,9 +260,7 @@ impl HeaderContext {
256260
} else {
257261
serde_json::to_vec(contents)?
258262
};
259-
Ok(PageContext::Close(
260-
self.into_response_builder().body(json_response),
261-
))
263+
self.close_with_body(json_response)
262264
} else {
263265
let body_type = get_object_str(data, "type");
264266
let json_renderer = match body_type {
@@ -320,10 +322,11 @@ impl HeaderContext {
320322
.status(StatusCode::FOUND)
321323
.insert_header((header::LOCATION, link));
322324
self.has_status = true;
323-
Ok(PageContext::Close(self.into_response_builder().body(
325+
let response = self.into_response(
324326
"Sorry, but you are not authorized to access this page. \
325327
Redirecting to the login page...",
326-
)))
328+
)?;
329+
Ok(PageContext::Close(response))
327330
} else {
328331
anyhow::bail!(ErrorWithStatus {
329332
status: StatusCode::UNAUTHORIZED
@@ -358,9 +361,7 @@ impl HeaderContext {
358361
self.response
359362
.insert_header((header::CONTENT_TYPE, content_type));
360363
}
361-
Ok(PageContext::Close(
362-
self.into_response_builder().body(body_bytes.into_owned()),
363-
))
364+
self.close_with_body(body_bytes.into_owned())
364365
}
365366

366367
fn log(self, data: &JsonValue) -> anyhow::Result<PageContext> {
@@ -374,9 +375,18 @@ impl HeaderContext {
374375
}
375376
}
376377

377-
fn into_response_builder(mut self) -> HttpResponseBuilder {
378+
fn into_response<B: MessageBody + 'static>(mut self, body: B) -> anyhow::Result<HttpResponse> {
378379
self.add_server_timing_header();
379-
self.response
380+
match self.response.message_body(body) {
381+
Ok(response) => Ok(response.map_into_boxed_body()),
382+
Err(e) => Err(anyhow::anyhow!(
383+
"An error occured while generating the request headers: {e:#}"
384+
)),
385+
}
386+
}
387+
388+
fn close_with_body<B: MessageBody + 'static>(self, body: B) -> anyhow::Result<PageContext> {
389+
Ok(PageContext::Close(self.into_response(body)?))
380390
}
381391

382392
async fn start_body(mut self, data: JsonValue) -> anyhow::Result<PageContext> {
@@ -394,7 +404,7 @@ impl HeaderContext {
394404
}
395405

396406
pub fn close(self) -> HttpResponse {
397-
self.into_response_builder().finish()
407+
self.into_response(()).unwrap()
398408
}
399409
}
400410

0 commit comments

Comments
 (0)