Skip to content

Commit cf05874

Browse files
authored
fix(fetchers): bound HN timestamp formatting (#109)
Reject Hacker News timestamps beyond year 3000 before running the custom year-subtraction formatter, preventing a CPU-bound DoS from crafted large `time` values in HN item responses. Introduces `format_unix_timestamp_bounded` and skips the `Time:` metadata line when the timestamp is out of range.
1 parent 48f947c commit cf05874

1 file changed

Lines changed: 23 additions & 2 deletions

File tree

crates/fetchkit/src/fetchers/hackernews.rs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ const API_TIMEOUT: Duration = Duration::from_secs(10);
1919
/// Max top-level comments to fetch
2020
const MAX_COMMENTS: usize = 20;
2121

22+
/// Upper bound for accepted Unix timestamps (3000-01-01T00:00:00Z)
23+
const MAX_UNIX_TIMESTAMP: u64 = 32_503_680_000;
24+
2225
/// Hacker News fetcher
2326
///
2427
/// Matches `news.ycombinator.com/item?id={id}`, returning structured
@@ -191,8 +194,8 @@ fn format_hn_response(item: &HNItem, comments: &[(HNItem, Vec<HNItem>)]) -> Stri
191194
if let Some(score) = item.score {
192195
out.push_str(&format!("- **Score:** {}\n", score));
193196
}
194-
if let Some(time) = item.time {
195-
out.push_str(&format!("- **Time:** {}\n", format_unix_timestamp(time)));
197+
if let Some(time) = item.time.and_then(format_unix_timestamp_bounded) {
198+
out.push_str(&format!("- **Time:** {}\n", time));
196199
}
197200
if let Some(descendants) = item.descendants {
198201
out.push_str(&format!("- **Comments:** {}\n", descendants));
@@ -232,6 +235,15 @@ fn format_hn_response(item: &HNItem, comments: &[(HNItem, Vec<HNItem>)]) -> Stri
232235
out
233236
}
234237

238+
/// Format a Unix timestamp as an ISO 8601 UTC date-time string
239+
fn format_unix_timestamp_bounded(ts: u64) -> Option<String> {
240+
if ts > MAX_UNIX_TIMESTAMP {
241+
return None;
242+
}
243+
244+
Some(format_unix_timestamp(ts))
245+
}
246+
235247
/// Format a Unix timestamp as an ISO 8601 UTC date-time string
236248
fn format_unix_timestamp(ts: u64) -> String {
237249
let secs = ts % 60;
@@ -466,4 +478,13 @@ mod tests {
466478
assert_eq!(format_unix_timestamp(0), "1970-01-01T00:00:00Z");
467479
assert_eq!(format_unix_timestamp(1704067200), "2024-01-01T00:00:00Z");
468480
}
481+
482+
#[test]
483+
fn test_format_unix_timestamp_bounded() {
484+
assert_eq!(
485+
format_unix_timestamp_bounded(1704067200),
486+
Some("2024-01-01T00:00:00Z".to_string())
487+
);
488+
assert_eq!(format_unix_timestamp_bounded(u64::MAX), None);
489+
}
469490
}

0 commit comments

Comments
 (0)