Skip to content

Commit 7bf6130

Browse files
authored
Merge pull request #84 from VibeEngineeringAI/fix/header-unwrap-panics
fix: reject malformed headers without panicking workers
2 parents 1329cf5 + 8d81412 commit 7bf6130

42 files changed

Lines changed: 3412 additions & 451 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎Cargo.lock‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ anyhow = "1.0"
221221
procfs = { version = "0.18.0", default-features = false }
222222

223223
[dev-dependencies]
224+
proptest = { version = "=1.9.0", default-features = false, features = ["std"] }
224225
rstest = "0.26.1"
225226
arrow = "59.2.0"
226227
temp-dir = "0.1.14"

‎src/alerts/mod.rs‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1263,6 +1263,8 @@ pub enum AlertError {
12631263
ValidationFailure(String),
12641264
#[error(transparent)]
12651265
MetastoreError(#[from] MetastoreError),
1266+
#[error("{0}")]
1267+
Header(#[from] crate::utils::header_parsing::ParseHeaderError),
12661268
}
12671269

12681270
impl From<outbound_http_policy::OutboundPolicyError> for AlertError {
@@ -1298,6 +1300,7 @@ impl actix_web::ResponseError for AlertError {
12981300
Self::NotPresentInOSS(_) => StatusCode::BAD_REQUEST,
12991301
Self::MetastoreError(_) => StatusCode::INTERNAL_SERVER_ERROR,
13001302
Self::OutboundPolicy(_) => StatusCode::BAD_REQUEST,
1303+
Self::Header(_) => StatusCode::BAD_REQUEST,
13011304
}
13021305
}
13031306

@@ -1972,3 +1975,19 @@ mod tests {
19721975
}
19731976
}
19741977
}
1978+
1979+
#[cfg(test)]
1980+
mod header_error_tests {
1981+
use actix_web::ResponseError;
1982+
1983+
use super::*;
1984+
use crate::utils::header_parsing::ParseHeaderError;
1985+
1986+
#[test]
1987+
fn header_error_is_bad_request() {
1988+
assert_eq!(
1989+
AlertError::from(ParseHeaderError::InvalidTenantId).status_code(),
1990+
StatusCode::BAD_REQUEST
1991+
);
1992+
}
1993+
}

‎src/apikeys.rs‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,36 @@ impl actix_web::ResponseError for ApiKeyError {
9898
#[cfg(test)]
9999
mod tests {
100100
use super::*;
101+
use crate::{
102+
handlers::http::rbac::RBACError,
103+
utils::header_parsing::{HeaderBuildError, ParseHeaderError},
104+
};
105+
use actix_web::{ResponseError, http::StatusCode};
101106
use serde_json::json;
102107

108+
#[test]
109+
fn sync_header_errors_preserve_status_without_revocation_wrapping() {
110+
let error = ApiKeyError::from(RBACError::HeaderBuild(HeaderBuildError::Internal(
111+
"configured username",
112+
)));
113+
assert_eq!(error.status_code(), StatusCode::INTERNAL_SERVER_ERROR);
114+
assert_eq!(
115+
error.to_string(),
116+
"Failed to build internal request headers"
117+
);
118+
119+
let error = ApiKeyError::from(RBACError::HeaderBuild(HeaderBuildError::Input(
120+
ParseHeaderError::InvalidTenantId,
121+
)));
122+
assert_eq!(error.status_code(), StatusCode::BAD_REQUEST);
123+
124+
let network_failure = ApiKeyError::RevocationSyncFailed("network failure".into());
125+
assert_eq!(
126+
network_failure.status_code(),
127+
StatusCode::SERVICE_UNAVAILABLE
128+
);
129+
}
130+
103131
#[test]
104132
fn ttl_accepts_inclusive_boundaries_and_no_expiration() {
105133
for ttl in [json!(null), json!(1800), json!(7_776_000)] {

‎src/event/format/known_schema.rs‎

Lines changed: 148 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ pub enum Error {
4343
"Unsupported log format: '{0}'. This format cannot be parsed by the current version. Please create an issue on our GitHub repository (github.com/parseablehq/parseable) with a sample log event, or reach out in the #support-channel on our Slack community for assistance. Include this error message and your log sample to help us improve compatibility."
4444
)]
4545
Unknown(String),
46+
#[error(
47+
"Event shape is not supported for known log formats. Expected a JSON object or an array of JSON objects."
48+
)]
49+
InvalidShape,
4650
}
4751

4852
/// Deserializes a string pattern into a compiled Regex
@@ -193,6 +197,8 @@ impl EventProcessor {
193197
/// # Returns
194198
/// * `Ok` - The original JSON will now contain extracted fields
195199
/// * `Err(Unacceptable)` - JSON provided is acceptable for the known format
200+
/// * `Err(Unknown)` - `log_source` is not a known format
201+
/// * `Err(InvalidShape)` - JSON is neither an object nor an array
196202
pub fn extract_from_inline_log(
197203
&self,
198204
json: &mut Value,
@@ -227,7 +233,9 @@ impl EventProcessor {
227233
p_custom_fields.insert(FORMAT_VERIFY_KEY.to_string(), "false".to_string());
228234
}
229235
}
230-
_ => unreachable!("We don't accept events of the form: {json}"),
236+
// Scalars (number, string, bool, null) can't carry log fields.
237+
// The value is deliberately not echoed back in the error.
238+
_ => return Err(Error::InvalidShape),
231239
}
232240

233241
Ok(fields)
@@ -470,6 +478,145 @@ mod tests {
470478
);
471479
}
472480

481+
#[test]
482+
fn test_extract_from_inline_log_rejects_scalars() {
483+
let processor = EventProcessor::new(TEST_CONFIG);
484+
485+
for scalar in [
486+
json!(42),
487+
json!(1.5),
488+
json!("[ERROR] [2023-10-10T13:55:36Z] do-not-echo-this"),
489+
json!(true),
490+
Value::Null,
491+
] {
492+
let mut json_value = scalar.clone();
493+
let mut p_custom_fields = HashMap::new();
494+
let result = processor.extract_from_inline_log(
495+
&mut json_value,
496+
&mut p_custom_fields,
497+
"custom_app_log",
498+
Some("raw_log"),
499+
);
500+
501+
let err = result.expect_err("scalar events must be rejected");
502+
assert!(matches!(err, Error::InvalidShape), "{scalar} gave {err:?}");
503+
assert!(!err.to_string().contains("do-not-echo-this"));
504+
assert_eq!(json_value, scalar, "scalar event must be left untouched");
505+
assert!(p_custom_fields.is_empty());
506+
assert_eq!(
507+
actix_web::ResponseError::status_code(
508+
&crate::handlers::http::ingest::PostError::from(err)
509+
),
510+
actix_web::http::StatusCode::BAD_REQUEST
511+
);
512+
}
513+
}
514+
515+
#[test]
516+
fn test_extract_from_inline_log_unknown_source_before_shape() {
517+
let processor = EventProcessor::new(TEST_CONFIG);
518+
let mut json_value = json!(42);
519+
let mut p_custom_fields = HashMap::new();
520+
521+
let result = processor.extract_from_inline_log(
522+
&mut json_value,
523+
&mut p_custom_fields,
524+
"nonexistent_format",
525+
Some("raw_log"),
526+
);
527+
528+
assert!(matches!(result, Err(Error::Unknown(name)) if name == "nonexistent_format"));
529+
}
530+
531+
#[test]
532+
fn test_extract_from_inline_log_object_via_processor() {
533+
let processor = EventProcessor::new(TEST_CONFIG);
534+
let mut json_value = json!({
535+
"raw_log": "[ERROR] [2023-10-10T13:55:36Z] Failed to connect to database"
536+
});
537+
let mut p_custom_fields = HashMap::new();
538+
539+
let fields = processor
540+
.extract_from_inline_log(
541+
&mut json_value,
542+
&mut p_custom_fields,
543+
"custom_app_log",
544+
Some("raw_log"),
545+
)
546+
.unwrap();
547+
548+
assert!(fields.contains("level"));
549+
assert_eq!(json_value["level"], "ERROR");
550+
assert!(p_custom_fields.is_empty());
551+
}
552+
553+
#[test]
554+
fn test_extract_from_inline_log_object_no_match_marks_unverified() {
555+
let processor = EventProcessor::new(TEST_CONFIG);
556+
let mut json_value = json!({ "raw_log": "not a known format" });
557+
let mut p_custom_fields = HashMap::new();
558+
559+
let fields = processor
560+
.extract_from_inline_log(
561+
&mut json_value,
562+
&mut p_custom_fields,
563+
"custom_app_log",
564+
Some("raw_log"),
565+
)
566+
.unwrap();
567+
568+
assert!(fields.is_empty());
569+
assert_eq!(
570+
p_custom_fields.get(FORMAT_VERIFY_KEY).map(String::as_str),
571+
Some("false")
572+
);
573+
}
574+
575+
#[test]
576+
fn test_extract_from_inline_log_array_skips_non_objects() {
577+
let processor = EventProcessor::new(TEST_CONFIG);
578+
let mut json_value = json!([
579+
42,
580+
"plain string",
581+
null,
582+
{ "raw_log": "[INFO] [2023-10-10T13:55:40Z] Application started" }
583+
]);
584+
let mut p_custom_fields = HashMap::new();
585+
586+
let fields = processor
587+
.extract_from_inline_log(
588+
&mut json_value,
589+
&mut p_custom_fields,
590+
"custom_app_log",
591+
Some("raw_log"),
592+
)
593+
.unwrap();
594+
595+
assert!(fields.contains("message"));
596+
assert_eq!(json_value[0], json!(42));
597+
assert_eq!(json_value[3]["level"], "INFO");
598+
assert!(p_custom_fields.is_empty());
599+
}
600+
601+
#[test]
602+
fn test_extract_from_inline_log_empty_array() {
603+
let processor = EventProcessor::new(TEST_CONFIG);
604+
let mut json_value = json!([]);
605+
let mut p_custom_fields = HashMap::new();
606+
607+
let fields = processor
608+
.extract_from_inline_log(
609+
&mut json_value,
610+
&mut p_custom_fields,
611+
"custom_app_log",
612+
Some("raw_log"),
613+
)
614+
.unwrap();
615+
616+
assert!(fields.is_empty());
617+
assert!(p_custom_fields.is_empty());
618+
}
619+
473620
#[test]
474621
fn test_unknown_log_format() {
475622
let processor = EventProcessor::new(TEST_CONFIG);

‎src/handlers/http/alert_target_policy.rs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use crate::utils::get_tenant_id_from_request;
1313
use actix_web::HttpRequest;
1414

1515
pub async fn get(req: HttpRequest) -> Result<impl Responder, AlertError> {
16-
let tenant_id = get_tenant_id_from_request(&req);
16+
let tenant_id = get_tenant_id_from_request(&req)?;
1717
let policy = active_policy(&tenant_id).await?;
1818
Ok(web::Json(policy))
1919
}
@@ -23,7 +23,7 @@ pub async fn put(
2323
Json(policy): Json<AlertTargetPolicyConfig>,
2424
) -> Result<impl Responder, AlertError> {
2525
// validate before replacing so a bad policy never becomes active
26-
let tenant_id = get_tenant_id_from_request(&req);
26+
let tenant_id = get_tenant_id_from_request(&req)?;
2727
validate_policy(&policy)?;
2828
replace_policy(&tenant_id, policy.clone()).await?;
2929
Ok(web::Json(policy))

‎src/handlers/http/alerts.rs‎

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ pub async fn post(
260260
req: HttpRequest,
261261
Json(alert): Json<AlertRequest>,
262262
) -> Result<impl Responder, AlertError> {
263-
let tenant_id = get_tenant_id_from_request(&req);
263+
let tenant_id = get_tenant_id_from_request(&req)?;
264264
let session_key = extract_session_key_from_req(&req)?;
265265
Ok(web::Json(
266266
post_internal(alert, &session_key, &tenant_id).await?,
@@ -356,7 +356,7 @@ pub async fn post_internal(
356356
pub async fn get(req: HttpRequest, alert_id: Path<Ulid>) -> Result<impl Responder, AlertError> {
357357
let session_key = extract_session_key_from_req(&req)?;
358358
let alert_id = alert_id.into_inner();
359-
let tenant_id = get_tenant_id_from_request(&req);
359+
let tenant_id = get_tenant_id_from_request(&req)?;
360360
Ok(web::Json(
361361
get_internal(&session_key, alert_id, &tenant_id).await?,
362362
))
@@ -388,7 +388,7 @@ pub async fn delete(req: HttpRequest, alert_id: Path<Ulid>) -> Result<impl Respo
388388
let session_key = extract_session_key_from_req(&req)?;
389389
let alert_id = alert_id.into_inner();
390390
let _lifecycle = crate::alerts::promql_alerts::lock_lifecycle(alert_id).await;
391-
let tenant_id = get_tenant_id_from_request(&req);
391+
let tenant_id = get_tenant_id_from_request(&req)?;
392392
let guard = ALERTS.write().await;
393393
let alerts = if let Some(alerts) = guard.as_ref() {
394394
alerts
@@ -435,7 +435,7 @@ pub async fn update_notification_state(
435435
let session_key = extract_session_key_from_req(&req)?;
436436
let alert_id = alert_id.into_inner();
437437
let _lifecycle = crate::alerts::promql_alerts::lock_lifecycle(alert_id).await;
438-
let tenant_id = get_tenant_id_from_request(&req);
438+
let tenant_id = get_tenant_id_from_request(&req)?;
439439
let new_notification_state = match new_notification_state.state.as_str() {
440440
"notify" => NotificationState::Notify,
441441
"indefinite" => NotificationState::Mute("indefinite".into()),
@@ -492,7 +492,7 @@ pub async fn disable_alert(
492492
) -> Result<impl Responder, AlertError> {
493493
let session_key = extract_session_key_from_req(&req)?;
494494
let alert_id = alert_id.into_inner();
495-
let tenant_id = get_tenant_id_from_request(&req);
495+
let tenant_id = get_tenant_id_from_request(&req)?;
496496
Ok(web::Json(
497497
disable_alert_internal(&session_key, alert_id, &tenant_id).await?,
498498
))
@@ -534,7 +534,7 @@ pub async fn enable_alert(
534534
) -> Result<impl Responder, AlertError> {
535535
let session_key = extract_session_key_from_req(&req)?;
536536
let alert_id = alert_id.into_inner();
537-
let tenant_id = get_tenant_id_from_request(&req);
537+
let tenant_id = get_tenant_id_from_request(&req)?;
538538
Ok(web::Json(
539539
enable_alert_internal(&session_key, alert_id, &tenant_id).await?,
540540
))
@@ -591,7 +591,7 @@ pub async fn modify_alert(
591591
let session_key = extract_session_key_from_req(&req)?;
592592
let alert_id = alert_id.into_inner();
593593
let _lifecycle = crate::alerts::promql_alerts::lock_lifecycle(alert_id).await;
594-
let tenant_id = get_tenant_id_from_request(&req);
594+
let tenant_id = get_tenant_id_from_request(&req)?;
595595
// Get alerts manager reference without holding the global lock
596596
let alerts = {
597597
let guard = ALERTS.read().await;
@@ -688,7 +688,7 @@ pub async fn evaluate_alert(
688688
) -> Result<impl Responder, AlertError> {
689689
let session_key = extract_session_key_from_req(&req)?;
690690
let alert_id = alert_id.into_inner();
691-
let tenant_id = get_tenant_id_from_request(&req);
691+
let tenant_id = get_tenant_id_from_request(&req)?;
692692
Ok(Json(
693693
evaluate_alert_internal(&session_key, alert_id, &tenant_id).await?,
694694
))
@@ -724,7 +724,7 @@ pub async fn evaluate_alert_internal(
724724
}
725725

726726
pub async fn list_tags(req: HttpRequest) -> Result<impl Responder, AlertError> {
727-
let tenant_id = get_tenant_id_from_request(&req);
727+
let tenant_id = get_tenant_id_from_request(&req)?;
728728
Ok(web::Json(list_tags_internal(&tenant_id).await?))
729729
}
730730

‎src/handlers/http/apikeys.rs‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,7 @@ pub async fn create_api_key(
331331
// can be slow and must not block other create/delete requests.
332332
drop(guard);
333333

334-
// Best-effort sync to other live nodes (reuses the standard user sync).
334+
// Node delivery is best effort; invalid forwarding headers still fail the request.
335335
let caller_userid = user
336336
.as_api_key()
337337
.map(|k| k.created_by.clone())
@@ -345,6 +345,9 @@ pub async fn create_api_key(
345345
)
346346
.await
347347
{
348+
if matches!(&e, RBACError::HeaderBuild(_)) {
349+
return Err(e.into());
350+
}
348351
tracing::error!("Failed to sync API-key user creation: {e}");
349352
}
350353

@@ -393,6 +396,9 @@ pub async fn delete_api_key(
393396
)
394397
.await
395398
{
399+
if matches!(&e, RBACError::HeaderBuild(_)) {
400+
return Err(e.into());
401+
}
396402
tracing::error!("Failed to sync API-key user deletion: {e}");
397403
return Err(ApiKeyError::RevocationSyncFailed(e.to_string()));
398404
}

0 commit comments

Comments
 (0)