Skip to content

Commit 8013c62

Browse files
cursoragentlovasoa
andcommitted
Feat: Sanitize header values to prevent injection attacks
Co-authored-by: contact <[email protected]>
1 parent c38a109 commit 8013c62

File tree

2 files changed

+37
-5
lines changed

2 files changed

+37
-5
lines changed

src/render.rs

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,9 @@ impl HeaderContext {
167167
self.response.status(StatusCode::FOUND);
168168
self.has_status = true;
169169
}
170-
self.response.insert_header((name.as_str(), value_str));
170+
let sanitized_value = sanitize_header_value(value_str);
171+
self.response
172+
.insert_header((name.as_str(), sanitized_value));
171173
}
172174
Ok(self)
173175
}
@@ -242,7 +244,9 @@ impl HeaderContext {
242244
self.has_status = true;
243245
let link = get_object_str(data, "link")
244246
.with_context(|| "The redirect component requires a 'link' property")?;
245-
self.response.insert_header((header::LOCATION, link));
247+
let sanitized_link = sanitize_header_value(link);
248+
self.response
249+
.insert_header((header::LOCATION, sanitized_link));
246250
let response = self.response.body(());
247251
Ok(response)
248252
}
@@ -288,9 +292,10 @@ impl HeaderContext {
288292
get_object_str(options, "filename").or_else(|| get_object_str(options, "title"))
289293
{
290294
let extension = if filename.contains('.') { "" } else { ".csv" };
295+
let sanitized_filename = sanitize_header_value(filename);
291296
self.response.insert_header((
292297
header::CONTENT_DISPOSITION,
293-
format!("attachment; filename={filename}{extension}"),
298+
format!("attachment; filename={sanitized_filename}{extension}"),
294299
));
295300
}
296301
let csv_renderer = CsvBodyRenderer::new(self.writer, options).await?;
@@ -315,9 +320,10 @@ impl HeaderContext {
315320
log::debug!("Authentication failed");
316321
// The authentication failed
317322
let http_response: HttpResponse = if let Some(link) = get_object_str(&data, "link") {
323+
let sanitized_link = sanitize_header_value(link);
318324
self.response
319325
.status(StatusCode::FOUND)
320-
.insert_header((header::LOCATION, link))
326+
.insert_header((header::LOCATION, sanitized_link))
321327
.body(
322328
"Sorry, but you are not authorized to access this page. \
323329
Redirecting to the login page...",
@@ -333,9 +339,10 @@ impl HeaderContext {
333339

334340
fn download(mut self, options: &JsonValue) -> anyhow::Result<PageContext> {
335341
if let Some(filename) = get_object_str(options, "filename") {
342+
let sanitized_filename = sanitize_header_value(filename);
336343
self.response.insert_header((
337344
header::CONTENT_DISPOSITION,
338-
format!("attachment; filename=\"{filename}\""),
345+
format!("attachment; filename=\"{sanitized_filename}\""),
339346
));
340347
}
341348
let data_url = get_object_str(options, "data_url")
@@ -407,6 +414,26 @@ async fn verify_password_async(
407414
.await?
408415
}
409416

417+
fn sanitize_header_value(value: &str) -> String {
418+
let sanitized: String = value
419+
.chars()
420+
.filter(|&c| {
421+
let byte = c as u32;
422+
byte >= 0x20 && byte != 0x7F
423+
})
424+
.collect();
425+
426+
if sanitized != value {
427+
log::warn!(
428+
"Sanitized header value by removing control characters. Original length: {}, Sanitized length: {}",
429+
value.len(),
430+
sanitized.len()
431+
);
432+
}
433+
434+
sanitized
435+
}
436+
410437
fn get_object_str<'a>(json: &'a JsonValue, key: &str) -> Option<&'a str> {
411438
json.as_object()
412439
.and_then(|obj| obj.get(key))
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
select 'redirect' as component, 'line1
2+
line2
3+
line3
4+
' as link;
5+
select 'text' as component, 'It works !' AS contents;

0 commit comments

Comments
 (0)