MUMBLE "failed to get access using private key": чекер держит share-токен, выданный ещё старым keygen, а после перехода на HMAC ValidateKey его отвергал. - keygen/legacy.go: восстановленный из keygen.a алгоритм seed=(seed*17+42)%62 принимается ТОЛЬКО для video.id <= cutoff, то есть для видео, существовавших на момент перехода. Выше cutoff -- лишь HMAC. - cutoff берётся из max(video.id) при первом старте и пишется в public/.legacy_cutoff на volume: рестарт не должен расширять окно. Не смогли прочитать -- fail closed, legacy выключен. - Когда старые флаги протухнут: echo 0 > public/.legacy_cutoff + рестарт, и legacy отключается полностью. Ещё MUMBLE "cannot create video" -- он же DoS, у NOP-команды то же самое: - checkGeometry сделан FAIL-OPEN. Неразобранный ffprobe больше не отклоняет загрузку, режем только успешно прочитанную и абсурдную геометрию. - ffmpeg -max_alloc 128M: 1080p кадру нужно ~8 МБ, бомбе ~1 ГБ. Работает даже когда probe промолчал. check_tiktak.sh: smoke-тест для vulnbox, гоняет сценарий чекера целиком и отдельно проверяет, что патчи реально в задеплоенном билде. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
89 lines
2.0 KiB
Go
89 lines
2.0 KiB
Go
package video
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"github.com/disintegration/imaging"
|
|
"image"
|
|
"image/png"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
var (
|
|
ProcessingDeadline = 2 * time.Second
|
|
// 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 {
|
|
// Reject decompression bombs before ffmpeg expands a frame into memory.
|
|
if err := checkGeometry(ctx, inp); err != nil {
|
|
return err
|
|
}
|
|
// -max_alloc caps a single ffmpeg allocation: a 1080p frame needs ~8 MB, a
|
|
// 16383x16383 bomb needs ~1 GB. Second line of defence behind checkGeometry,
|
|
// and it works even when the probe told us nothing.
|
|
cmd := exec.CommandContext(ctx, "ffmpeg", "-y", "-threads", "1",
|
|
"-max_alloc", "134217728", "-i", inp, "-vframes", "1", out)
|
|
s := strings.Builder{}
|
|
cmd.Stdout = &s
|
|
cmd.Stderr = &s
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
fmt.Println(s.String())
|
|
}
|
|
return err
|
|
}
|
|
|
|
func GeneratePreview(ctx context.Context, inp string, out string) error {
|
|
select {
|
|
case semaphore <- struct{}{}:
|
|
ctx, cancel := context.WithTimeout(ctx, ProcessingDeadline)
|
|
defer func() {
|
|
cancel()
|
|
<-semaphore
|
|
}()
|
|
return generatePreview(ctx, inp, out)
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
}
|
|
|
|
func Blur(inp string, out string) error {
|
|
f, err := os.Open(inp)
|
|
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
|
|
}
|
|
outimg := imaging.Blur(img, 5)
|
|
outfile, err := os.Create(out)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer outfile.Close()
|
|
return png.Encode(outfile, outimg)
|
|
}
|