@marcel91200 / caddy Mirror · read-only / commits / d6f7f18b
Fork 0

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 {