fix(artwork): reject malformed hashes in ImageStore operations

Known-absent states carry an empty hash and malformed persisted hashes could panic path sharding or inject separators; Write/Open/Remove now return an error for anything but 16 lowercase hex chars.
This commit is contained in:
Deluan 2026-07-22 00:51:13 -04:00
parent 0147cc59b1
commit ebbe533c6a
2 changed files with 32 additions and 0 deletions

View File

@ -47,11 +47,28 @@ func extForMime(m string) string {
return ".img"
}
// validHash rejects anything but 16 lowercase hex chars: known-absent states carry "",
// and malformed persisted hashes must never reach path sharding (slice panics, separators).
func validHash(hash string) bool {
if len(hash) != 16 {
return false
}
for _, c := range []byte(hash) {
if (c < '0' || c > '9') && (c < 'a' || c > 'f') {
return false
}
}
return true
}
func (s *ImageStore) path(hash, mimeType string) string {
return filepath.Join(s.root, hash[0:2], hash[2:4], hash+extForMime(mimeType))
}
func (s *ImageStore) Write(hash, mimeType string, r io.Reader) error {
if !validHash(hash) {
return fmt.Errorf("imagestore: invalid hash %q", hash)
}
dst := s.path(hash, mimeType)
if _, err := os.Stat(dst); err == nil {
// A touched mtime marks the file live so a concurrent prune spares it.
@ -80,12 +97,18 @@ func (s *ImageStore) Write(hash, mimeType string, r io.Reader) error {
}
func (s *ImageStore) Open(hash, mimeType string) (io.ReadCloser, error) {
if !validHash(hash) {
return nil, fmt.Errorf("imagestore: invalid hash %q", hash)
}
return os.Open(s.path(hash, mimeType))
}
// Remove deletes the store file unless it is newer than olderThan, in which case
// an overlapping acquisition may have just touched it and be about to commit its row.
func (s *ImageStore) Remove(hash, mimeType string, olderThan time.Time) error {
if !validHash(hash) {
return fmt.Errorf("imagestore: invalid hash %q", hash)
}
path := s.path(hash, mimeType)
info, err := os.Stat(path)
if errors.Is(err, fs.ErrNotExist) {

View File

@ -90,6 +90,15 @@ var _ = Describe("ImageStore", func() {
Expect(store.Remove("beefbeefbeefbeef", "image/jpeg", time.Now())).To(Succeed())
})
It("rejects invalid hashes instead of panicking", func() {
for _, h := range []string{"", "ab", "BEEFBEEFBEEFBEEF", "../../../../etcpw", "beefbeefbeefbee/"} {
Expect(store.Write(h, "image/jpeg", bytes.NewReader([]byte("x")))).To(MatchError(ContainSubstring("invalid hash")))
_, err := store.Open(h, "image/jpeg")
Expect(err).To(MatchError(ContainSubstring("invalid hash")))
Expect(store.Remove(h, "image/jpeg", time.Now())).To(MatchError(ContainSubstring("invalid hash")))
}
})
It("spares a file newer than the cutoff, removes an aged one", func() {
fresh := []byte("fresh")
hf, _ := HashImage(bytes.NewReader(fresh))