package api import ( "bytes" "encoding/xml" "io" "mime" "net/http" "os" "path" "strconv" "strings" "syscall" ) const maxImageBytes = 16 << 20 func (a *API) wsImage(w http.ResponseWriter, r *http.Request, root *os.Root) { if r.Method != http.MethodGet { fail(w, 405, "method not allowed") return } p := r.URL.Query().Get("path") if p == "" || path.IsAbs(p) || p == "." || p == ".." || strings.HasPrefix(p, "../") || path.Clean(p) != p || strings.ContainsRune(p, 0) { fail(w, 400, "invalid path") return } anchor, err := root.Open(".") if err != nil { fail(w, 500, "storage unavailable") return } defer anchor.Close() f, err := openDownload(anchor, p) if err != nil { fail(w, 404, "image unavailable") return } defer f.Close() st, err := f.Stat() if err != nil || !st.Mode().IsRegular() { fail(w, 415, "unsupported image") return } if st.Size() > maxImageBytes { fail(w, 413, "image too large") return } b, err := io.ReadAll(io.LimitReader(f, maxImageBytes+1)) if err != nil { fail(w, 500, "image read failed") return } if len(b) > maxImageBytes { fail(w, 413, "image too large") return } after, err := f.Stat() if err != nil || int64(len(b)) != st.Size() || st.Size() != after.Size() || !st.ModTime().Equal(after.ModTime()) || st.Sys().(*syscall.Stat_t).Ctim != after.Sys().(*syscall.Stat_t).Ctim { fail(w, 409, "image changed; reload") return } mimeType := workspaceImageType(p, b) if mimeType == "" { fail(w, 415, "unsupported image") return } w.Header().Set("Content-Type", mimeType) w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("Content-Security-Policy", "sandbox; default-src 'none'; style-src 'unsafe-inline'") w.Header().Set("Content-Disposition", mime.FormatMediaType("attachment", map[string]string{"filename": path.Base(p)})) w.Header().Set("Cache-Control", "no-store") w.Header().Set("Content-Length", strconv.Itoa(len(b))) w.Write(b) } func workspaceImageType(name string, b []byte) string { switch mimeType := http.DetectContentType(b); mimeType { case "image/png", "image/jpeg", "image/gif", "image/webp": return mimeType } if !strings.EqualFold(path.Ext(name), ".svg") { return "" } d := xml.NewDecoder(bytes.NewReader(bytes.TrimPrefix(b, []byte{0xef, 0xbb, 0xbf}))) depth, roots := 0, 0 for { token, err := d.Token() if err == io.EOF { if roots == 1 && depth == 0 { return "image/svg+xml" } return "" } if err != nil { return "" } switch t := token.(type) { case xml.StartElement: if depth == 0 { roots++ if roots != 1 || t.Name.Local != "svg" || t.Name.Space != "http://www.w3.org/2000/svg" { return "" } } depth++ case xml.EndElement: depth-- case xml.CharData: if depth == 0 && len(bytes.TrimSpace(t)) != 0 { return "" } case xml.Directive: return "" } } }