Migration This is my new public git source viewer. Source coming soon. Please contact with concerns/bugs. Contact me

internal/modules/image.go

4bda28df3ab3dbfcc789dcbbb184cabc8cac9398 · 2.7 KB · 72 lines raw

1 package modules
2
3 import (
4 "bytes"
5 "path"
6 "strings"
7 )
8
9 // maxPreviewSize bounds how large an image the blob view will render inline.
10 // Past it the view keeps the download link but drops the preview, so a large
11 // file is not pulled over the wire twice (once as the page's subresource,
12 // once as the bytes the reader actually wanted). The /raw/ route itself is
13 // not capped here: a direct request is an explicit request for the bytes,
14 // and MaxGit still bounds every git read -- it must stay above this value or
15 // a previewable-sized image would already be truncated by the time it gets
16 // here.
17 const maxPreviewSize = 10 << 20 // 10 MiB
18
19 // imageContentType returns the MIME type to serve at p when its leading
20 // bytes are that format and the extension agrees, and "" otherwise.
21 //
22 // SECURITY: this is the one place /raw/ is allowed to declare a non-text
23 // Content-Type, so the rules are deliberately narrow:
24 // - only raster formats are recognised. SVG is an XML document that can
25 // carry <script>, so it is never in this list and keeps the text/plain
26 // fallback; the danger is top-level navigation to an image/svg+xml
27 // response on this origin, not <img>, where scripts do not run.
28 // - the extension and the magic bytes must BOTH match, so an .png whose
29 // contents are really HTML is not relabelled as an image. Relaxing the
30 // current attachment rule for attacker-influenced bytes is only safe
31 // because a mislabelled file fails here and keeps the download.
32 // - the caller still sends X-Content-Type-Options: nosniff and the
33 // sandbox CSP, so even a polyglot declared image/* cannot be re-sniffed
34 // as a document.
35 func imageContentType(p string, data []byte) string {
36 switch strings.ToLower(path.Ext(p)) {
37 case ".png":
38 if bytes.HasPrefix(data, []byte("\x89PNG\r\n\x1a\n")) {
39 return "image/png"
40 }
41 case ".jpg", ".jpeg":
42 if bytes.HasPrefix(data, []byte("\xff\xd8\xff")) {
43 return "image/jpeg"
44 }
45 case ".gif":
46 if bytes.HasPrefix(data, []byte("GIF87a")) || bytes.HasPrefix(data, []byte("GIF89a")) {
47 return "image/gif"
48 }
49 case ".webp":
50 if len(data) >= 12 && bytes.Equal(data[:4], []byte("RIFF")) &&
51 bytes.Equal(data[8:12], []byte("WEBP")) {
52 return "image/webp"
53 }
54 case ".bmp":
55 if bytes.HasPrefix(data, []byte("BM")) {
56 return "image/bmp"
57 }
58 case ".ico":
59 if bytes.HasPrefix(data, []byte("\x00\x00\x01\x00")) {
60 return "image/x-icon"
61 }
62 case ".avif":
63 // ISO-BMFF: "ftyp" at offset 4, major brand avif/avis at 8. A file
64 // with a compatible-but-not-major avif brand is left unlabelled
65 // rather than guessed at.
66 if len(data) >= 12 && bytes.Equal(data[4:8], []byte("ftyp")) &&
67 (bytes.Equal(data[8:12], []byte("avif")) || bytes.Equal(data[8:12], []byte("avis"))) {
68 return "image/avif"
69 }
70 }
71 return ""
72 }