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

internal/modules/image.go

38fa4e6cd9bb9dbf0ae23ef02e50b1c75cd84ebb · 2.6 KB · 70 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.
15 const maxPreviewSize = 2 << 20 // 2 MiB
16
17 // imageContentType returns the MIME type to serve at p when its leading
18 // bytes are that format and the extension agrees, and "" otherwise.
19 //
20 // SECURITY: this is the one place /raw/ is allowed to declare a non-text
21 // Content-Type, so the rules are deliberately narrow:
22 // - only raster formats are recognised. SVG is an XML document that can
23 // carry <script>, so it is never in this list and keeps the text/plain
24 // fallback; the danger is top-level navigation to an image/svg+xml
25 // response on this origin, not <img>, where scripts do not run.
26 // - the extension and the magic bytes must BOTH match, so an .png whose
27 // contents are really HTML is not relabelled as an image. Relaxing the
28 // current attachment rule for attacker-influenced bytes is only safe
29 // because a mislabelled file fails here and keeps the download.
30 // - the caller still sends X-Content-Type-Options: nosniff and the
31 // sandbox CSP, so even a polyglot declared image/* cannot be re-sniffed
32 // as a document.
33 func imageContentType(p string, data []byte) string {
34 switch strings.ToLower(path.Ext(p)) {
35 case ".png":
36 if bytes.HasPrefix(data, []byte("\x89PNG\r\n\x1a\n")) {
37 return "image/png"
38 }
39 case ".jpg", ".jpeg":
40 if bytes.HasPrefix(data, []byte("\xff\xd8\xff")) {
41 return "image/jpeg"
42 }
43 case ".gif":
44 if bytes.HasPrefix(data, []byte("GIF87a")) || bytes.HasPrefix(data, []byte("GIF89a")) {
45 return "image/gif"
46 }
47 case ".webp":
48 if len(data) >= 12 && bytes.Equal(data[:4], []byte("RIFF")) &&
49 bytes.Equal(data[8:12], []byte("WEBP")) {
50 return "image/webp"
51 }
52 case ".bmp":
53 if bytes.HasPrefix(data, []byte("BM")) {
54 return "image/bmp"
55 }
56 case ".ico":
57 if bytes.HasPrefix(data, []byte("\x00\x00\x01\x00")) {
58 return "image/x-icon"
59 }
60 case ".avif":
61 // ISO-BMFF: "ftyp" at offset 4, major brand avif/avis at 8. A file
62 // with a compatible-but-not-major avif brand is left unlabelled
63 // rather than guessed at.
64 if len(data) >= 12 && bytes.Equal(data[4:8], []byte("ftyp")) &&
65 (bytes.Equal(data[8:12], []byte("avif")) || bytes.Equal(data[8:12], []byte("avis"))) {
66 return "image/avif"
67 }
68 }
69 return ""
70 }