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 | } |