Migration
This is my new public git source viewer. Source coming soon. Please contact with concerns/bugs.
Contact me
internal/modules/image.go
cb5a7c7c656de5a381e0234208c552c0856e43e3
· 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 | } |