mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-08-03 03:13:05 -04:00
cap delta-seconds values per RFC 9111
Huge max-age/Age header values previously either panicked in the i64 casts when storing metadata, or were silently truncated to their first 8 digits by the fixed-size lowercase buffer in CacheControl.parse. Cap them at 2^31 as RFC 9111 §1.2.2 prescribes and compare directives case-insensitively without truncation.
This commit is contained in:
committed by
Muki Kiboigo
parent
129061d627
commit
dae03cf9ba
54
src/network/cache/Cache.zig
vendored
54
src/network/cache/Cache.zig
vendored
@@ -67,6 +67,19 @@ pub fn clear(self: *Cache) !void {
|
||||
};
|
||||
}
|
||||
|
||||
/// RFC 9111 delta-seconds values larger than this are capped rather than
|
||||
/// rejected (§1.2.2). Capping also keeps the value safely castable to i64
|
||||
/// for freshness arithmetic and storage.
|
||||
const max_delta_seconds: u64 = 2147483648;
|
||||
|
||||
fn parseDeltaSeconds(value: []const u8) ?u64 {
|
||||
const seconds = std.fmt.parseInt(u64, value, 10) catch |err| switch (err) {
|
||||
error.Overflow => return max_delta_seconds,
|
||||
error.InvalidCharacter => return null,
|
||||
};
|
||||
return @min(seconds, max_delta_seconds);
|
||||
}
|
||||
|
||||
pub const CacheControl = struct {
|
||||
max_age: u64,
|
||||
must_revalidate: bool = false,
|
||||
@@ -79,16 +92,12 @@ pub const CacheControl = struct {
|
||||
|
||||
var iter = std.mem.splitScalar(u8, value, ',');
|
||||
while (iter.next()) |part| {
|
||||
const stripped = std.mem.trim(u8, part, &std.ascii.whitespace);
|
||||
const directive = std.mem.trim(u8, part, &std.ascii.whitespace);
|
||||
|
||||
var buf: [16]u8 = undefined;
|
||||
const len = @min(buf.len, stripped.len);
|
||||
const directive = std.ascii.lowerString(buf[0..len], stripped[0..len]);
|
||||
|
||||
if (std.mem.eql(u8, directive, "no-store")) {
|
||||
if (std.ascii.eqlIgnoreCase(directive, "no-store")) {
|
||||
return null;
|
||||
}
|
||||
if (std.mem.eql(u8, directive, "no-cache")) {
|
||||
if (std.ascii.eqlIgnoreCase(directive, "no-cache")) {
|
||||
if (!max_age_set) {
|
||||
cc.max_age = 0;
|
||||
max_age_set = true;
|
||||
@@ -97,19 +106,19 @@ pub const CacheControl = struct {
|
||||
cc.must_revalidate = true;
|
||||
continue;
|
||||
}
|
||||
if (std.mem.eql(u8, directive, "private")) {
|
||||
if (std.ascii.eqlIgnoreCase(directive, "private")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (std.mem.startsWith(u8, directive, "max-age=")) {
|
||||
if (std.ascii.startsWithIgnoreCase(directive, "max-age=")) {
|
||||
if (!max_s_age_set) {
|
||||
if (std.fmt.parseInt(u64, directive[8..], 10) catch null) |max_age| {
|
||||
if (parseDeltaSeconds(directive[8..])) |max_age| {
|
||||
cc.max_age = max_age;
|
||||
max_age_set = true;
|
||||
}
|
||||
}
|
||||
} else if (std.mem.startsWith(u8, directive, "s-maxage=")) {
|
||||
if (std.fmt.parseInt(u64, directive[9..], 10) catch null) |max_age| {
|
||||
} else if (std.ascii.startsWithIgnoreCase(directive, "s-maxage=")) {
|
||||
if (parseDeltaSeconds(directive[9..])) |max_age| {
|
||||
cc.max_age = max_age;
|
||||
max_age_set = true;
|
||||
max_s_age_set = true;
|
||||
@@ -184,7 +193,7 @@ pub const CachedMetadata = struct {
|
||||
const value = h.value;
|
||||
|
||||
if (std.ascii.eqlIgnoreCase("Age", name)) {
|
||||
self.age_at_store = std.fmt.parseInt(u64, value, 10) catch 0;
|
||||
self.age_at_store = parseDeltaSeconds(value) orelse 0;
|
||||
} else if (std.ascii.eqlIgnoreCase("Cache-Control", name)) {
|
||||
self.cache_control = CacheControl.parse(value) orelse continue;
|
||||
} else if (std.ascii.eqlIgnoreCase("ETag", name)) {
|
||||
@@ -300,7 +309,7 @@ pub fn tryCache(
|
||||
.content_type = if (content_type) |ct| try arena.dupe(u8, ct) else "application/octet-stream",
|
||||
.status = status,
|
||||
.stored_at = timestamp,
|
||||
.age_at_store = if (age) |a| std.fmt.parseInt(u64, a, 10) catch 0 else 0,
|
||||
.age_at_store = if (age) |a| parseDeltaSeconds(a) orelse 0 else 0,
|
||||
.cache_control = cc,
|
||||
.headers = &.{},
|
||||
.vary_headers = &.{},
|
||||
@@ -342,6 +351,15 @@ test "Cache: CacheControl.parse" {
|
||||
|
||||
try testing.expectEqual(null, CacheControl.parse("max-age=abc"));
|
||||
try testing.expectEqual(null, CacheControl.parse("max-age="));
|
||||
|
||||
// values longer than 8 digits must not be truncated
|
||||
try testing.expectEqual(315360000, CacheControl.parse("max-age=315360000").?.max_age);
|
||||
|
||||
// delta-seconds too large to represent are capped at 2^31 (RFC 9111 §1.2.2)
|
||||
try testing.expectEqual(max_delta_seconds, CacheControl.parse("max-age=2147483649").?.max_age);
|
||||
try testing.expectEqual(max_delta_seconds, CacheControl.parse("max-age=9999999999999999999").?.max_age);
|
||||
try testing.expectEqual(max_delta_seconds, CacheControl.parse("max-age=99999999999999999999999").?.max_age);
|
||||
try testing.expectEqual(max_delta_seconds, CacheControl.parse("s-maxage=9999999999999999999").?.max_age);
|
||||
}
|
||||
|
||||
test "Cache: CachedMetadata.renew updates timestamp and age" {
|
||||
@@ -381,6 +399,14 @@ test "Cache: CachedMetadata.renew updates age from Age header" {
|
||||
});
|
||||
|
||||
try testing.expectEqual(42, meta.age_at_store);
|
||||
|
||||
meta.renew(.{
|
||||
.url = "https://example.com",
|
||||
.timestamp = 2000,
|
||||
.headers = &.{.{ .name = "Age", .value = "18446744073709551615" }},
|
||||
});
|
||||
|
||||
try testing.expectEqual(max_delta_seconds, meta.age_at_store);
|
||||
}
|
||||
|
||||
test "Cache: CachedMetadata.renew updates cache_control" {
|
||||
|
||||
Reference in New Issue
Block a user