Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 11, 07:15 UTC
Merge commit from fork
* fileserver: add failing test for calculateEtag collision calculateEtag concatenates base36(mtime) and base36(size) with no separator, so distinct (mtime, size) pairs can yield identical digit strings and thus identical ETags. * fileserver: prevent ETag collisions by separating mtime and size components calculateEtag concatenated base36(mtime) and base36(size) with no separator, so distinct (mtime, size) pairs could decode to the same digit string and produce identical ETags.
Kévin Dunglas committed
Aug 11, 2026 at 21:23 UTC
d6f7f18b041de650b9a35ffc5d791fdcc7cb5049
2 files changed
+29
modules/caddyhttp/fileserver/staticfiles.go
+1
@@ -768,6 +768,7 @@ func calculateEtag(d os.FileInfo) string {
768
var sb strings.Builder
769
sb.WriteRune('"')
770
sb.WriteString(strconv.FormatInt(mtime.UnixNano(), 36))
771
+ sb.WriteRune('-')
772
sb.WriteString(strconv.FormatInt(d.Size(), 36))
773
sb.WriteRune('"')
774
return sb.String()
modules/caddyhttp/fileserver/staticfiles_test.go
+28
@@ -189,6 +189,34 @@ func check_validator_headers(modTime time.Time, expect_headers bool, t *testing.
189
}
190
}
191
192
+// calculateEtag concatenates the base-36 mtime and size with no separator,
193
+// so distinct (mtime, size) pairs can produce the same digit string and
194
+// therefore the same ETag.
195
+func TestCalculateEtagCollision(t *testing.T) {
196
+ fileA := fakeFileInfo{size: 75, modTime: time.Unix(2, 0)}
197
+ fileB := fakeFileInfo{size: 3, modTime: time.Unix(72, 2)}
198
+
199
+ etagA := calculateEtag(fileA)
200
+ etagB := calculateEtag(fileB)
201
+
202
+ if etagA == etagB {
203
+ t.Fatalf("etag collision: distinct files (size=%d mtime=%s) and (size=%d mtime=%s) both produced ETag %s",
204
+ fileA.size, fileA.modTime, fileB.size, fileB.modTime, etagA)
205
+ }
206
+}
207
+
208
+type fakeFileInfo struct {
209
+ size int64
210
+ modTime time.Time
211
+}
212
+
213
+func (f fakeFileInfo) Name() string { return "fake" }
214
+func (f fakeFileInfo) Size() int64 { return f.size }
215
+func (f fakeFileInfo) Mode() os.FileMode { return 0 }
216
+func (f fakeFileInfo) ModTime() time.Time { return f.modTime }
217
+func (f fakeFileInfo) IsDir() bool { return false }
218
+func (f fakeFileInfo) Sys() any { return nil }
219
+
220
func TestPrecompressedRangeResponse(t *testing.T) {
221
root := t.TempDir()
222
if err := os.WriteFile(filepath.Join(root, "range.txt"), []byte("original response body"), 0o600); err != nil {