diff --git a/services/tiktak/server/server.go b/services/tiktak/server/server.go index ea818a3..b7f2e2d 100644 --- a/services/tiktak/server/server.go +++ b/services/tiktak/server/server.go @@ -23,6 +23,10 @@ func NewServer(db *db.Storage, vs *video.Storage, c Config) *Server { t := Template{template.Must(template.ParseGlob(c.TemplatesFolder + "/*.html"))} s.e.Renderer = &t s.e.Use(middleware.Recover()) + // Hard cap on the whole request. The video file is size-checked separately, + // but nothing bounded the form fields: a huge subtitles blob becomes one + // slice entry per line inside GenerateVtt. + s.e.Use(middleware.BodyLimit("8M")) s.e.Use(func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { cc := &auth.Context{Context: c} @@ -111,6 +115,9 @@ func (s *Server) handleCreate(c echo.Context) error { descr := c.FormValue("description") subt := c.FormValue("subtitles") + if len(subt) > video.MaxSubtitlesSize { + return c.Render(http.StatusUnprocessableEntity, "create", ErrorResponse{"subtitles are too long"}) + } isPrivate := c.FormValue("private") v := db.Video{UserID: uid, Description: descr, Private: isPrivate == "on", Link: link} diff --git a/services/tiktak/video/preview.go b/services/tiktak/video/preview.go index db731d2..32b6b04 100644 --- a/services/tiktak/video/preview.go +++ b/services/tiktak/video/preview.go @@ -6,6 +6,7 @@ import ( "github.com/disintegration/imaging" "image" "image/png" + "io" "os" "os/exec" "strings" @@ -14,11 +15,17 @@ import ( var ( ProcessingDeadline = 2 * time.Second - semaphore = make(chan struct{}, 75) + // 75 concurrent ffmpeg processes are enough to swap the box out on their + // own, bomb or no bomb. + semaphore = make(chan struct{}, 16) ) func generatePreview(ctx context.Context, inp string, out string) error { - cmd := exec.CommandContext(ctx, "ffmpeg", "-y", "-i", inp, "-vframes", "1", out) + // Reject decompression bombs before ffmpeg expands a frame into memory. + if err := checkGeometry(ctx, inp); err != nil { + return err + } + cmd := exec.CommandContext(ctx, "ffmpeg", "-y", "-threads", "1", "-i", inp, "-vframes", "1", out) s := strings.Builder{} cmd.Stdout = &s cmd.Stderr = &s @@ -48,6 +55,21 @@ func Blur(inp string, out string) error { if err != nil { return err } + defer f.Close() + + // Same guard on the decode side: imaging.Blur keeps several copies of the + // bitmap alive, so an oversized preview is an OOM all by itself. + cfg, _, err := image.DecodeConfig(f) + if err != nil { + return err + } + if cfg.Width*cfg.Height > MaxPixels { + return fmt.Errorf("preview %dx%d is too large to blur", cfg.Width, cfg.Height) + } + if _, err := f.Seek(0, io.SeekStart); err != nil { + return err + } + img, _, err := image.Decode(f) if err != nil { return err diff --git a/services/tiktak/video/probe.go b/services/tiktak/video/probe.go new file mode 100644 index 0000000..b73b579 --- /dev/null +++ b/services/tiktak/video/probe.go @@ -0,0 +1,36 @@ +package video + +import ( + "context" + "fmt" + "os/exec" + "strings" +) + +// Decompression-bomb guard. A 100 KB webm can carry a 16383x16383 VP8 frame +// (the format's maximum): tiny on disk, ~1 GB once ffmpeg expands it to raw +// pixels. Probing the container header is cheap -- ffprobe reads metadata and +// never decodes a frame -- so we reject absurd geometry before any decoding. +const ( + MaxDimension = 4096 + MaxPixels = 8 << 20 // 8 Mpx, ~4x a 1080p frame +) + +func checkGeometry(ctx context.Context, inp string) error { + args := []string{"-v", "error", "-select_streams", "v:0", + "-show_entries", "stream=width,height", "-of", "csv=s=x:p=0", inp} + + out, err := exec.CommandContext(ctx, "ffprobe", args...).Output() + if err != nil { + return fmt.Errorf("cannot probe video: %v", err) + } + + var w, h int + if _, err := fmt.Sscanf(strings.TrimSpace(string(out)), "%dx%d", &w, &h); err != nil { + return fmt.Errorf("cannot read video geometry") + } + if w <= 0 || h <= 0 || w > MaxDimension || h > MaxDimension || w*h > MaxPixels { + return fmt.Errorf("video resolution %dx%d is not supported", w, h) + } + return nil +} diff --git a/services/tiktak/video/storage.go b/services/tiktak/video/storage.go index f9dfc82..2d8ad82 100644 --- a/services/tiktak/video/storage.go +++ b/services/tiktak/video/storage.go @@ -12,6 +12,9 @@ import ( const ( MaxSize = 5 << 20 + + // GenerateVtt splits this on \n and allocates one slice entry per line. + MaxSubtitlesSize = 64 << 10 ) var (