[security] Implement allowFiles fs for better isolation of ffmpeg / ffprobe (#3251)

* [chore] Implement readOneFile fs

* further isolation

* remove fmt call

* tweaks
This commit is contained in:
tobi 2024-08-30 14:03:59 +02:00 committed by GitHub
parent e10aa76612
commit cd93a5baf3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 104 additions and 31 deletions

View file

@ -21,6 +21,7 @@
"context" "context"
"encoding/json" "encoding/json"
"errors" "errors"
"os"
"path" "path"
"strconv" "strconv"
"strings" "strings"
@ -39,10 +40,7 @@
// any metadata encoded into the media stream itself will not be cleared. This is the best we // any metadata encoded into the media stream itself will not be cleared. This is the best we
// can do without absolutely tanking performance by requiring transcodes :( // can do without absolutely tanking performance by requiring transcodes :(
func ffmpegClearMetadata(ctx context.Context, outpath, inpath string) error { func ffmpegClearMetadata(ctx context.Context, outpath, inpath string) error {
// Get directory from filepath. return ffmpeg(ctx, inpath, outpath,
dirpath := path.Dir(inpath)
return ffmpeg(ctx, dirpath,
// Only log errors. // Only log errors.
"-loglevel", "error", "-loglevel", "error",
@ -66,18 +64,15 @@ func ffmpegClearMetadata(ctx context.Context, outpath, inpath string) error {
} }
// ffmpegGenerateWebpThumb generates a thumbnail webp from input media of any type, useful for any media. // ffmpegGenerateWebpThumb generates a thumbnail webp from input media of any type, useful for any media.
func ffmpegGenerateWebpThumb(ctx context.Context, filepath, outpath string, width, height int, pixfmt string) error { func ffmpegGenerateWebpThumb(ctx context.Context, inpath, outpath string, width, height int, pixfmt string) error {
// Get directory from filepath.
dirpath := path.Dir(filepath)
// Generate thumb with ffmpeg. // Generate thumb with ffmpeg.
return ffmpeg(ctx, dirpath, return ffmpeg(ctx, inpath, outpath,
// Only log errors. // Only log errors.
"-loglevel", "error", "-loglevel", "error",
// Input file. // Input file.
"-i", filepath, "-i", inpath,
// Encode using libwebp. // Encode using libwebp.
// (NOT as libwebp_anim). // (NOT as libwebp_anim).
@ -116,27 +111,24 @@ func ffmpegGenerateWebpThumb(ctx context.Context, filepath, outpath string, widt
} }
// ffmpegGenerateStatic generates a static png from input image of any type, useful for emoji. // ffmpegGenerateStatic generates a static png from input image of any type, useful for emoji.
func ffmpegGenerateStatic(ctx context.Context, filepath string) (string, error) { func ffmpegGenerateStatic(ctx context.Context, inpath string) (string, error) {
var outpath string var outpath string
// Generate thumb output path REPLACING extension. // Generate thumb output path REPLACING extension.
if i := strings.IndexByte(filepath, '.'); i != -1 { if i := strings.IndexByte(inpath, '.'); i != -1 {
outpath = filepath[:i] + "_static.png" outpath = inpath[:i] + "_static.png"
} else { } else {
return "", gtserror.New("input file missing extension") return "", gtserror.New("input file missing extension")
} }
// Get directory from filepath.
dirpath := path.Dir(filepath)
// Generate static with ffmpeg. // Generate static with ffmpeg.
if err := ffmpeg(ctx, dirpath, if err := ffmpeg(ctx, inpath, outpath,
// Only log errors. // Only log errors.
"-loglevel", "error", "-loglevel", "error",
// Input file. // Input file.
"-i", filepath, "-i", inpath,
// Only first frame. // Only first frame.
"-frames:v", "1", "-frames:v", "1",
@ -157,18 +149,45 @@ func ffmpegGenerateStatic(ctx context.Context, filepath string) (string, error)
return outpath, nil return outpath, nil
} }
// ffmpeg calls `ffmpeg [args...]` (WASM) with directory path mounted in runtime. // ffmpeg calls `ffmpeg [args...]` (WASM) with in + out paths mounted in runtime.
func ffmpeg(ctx context.Context, dirpath string, args ...string) error { func ffmpeg(ctx context.Context, inpath string, outpath string, args ...string) error {
var stderr byteutil.Buffer var stderr byteutil.Buffer
rc, err := _ffmpeg.Ffmpeg(ctx, _ffmpeg.Args{ rc, err := _ffmpeg.Ffmpeg(ctx, _ffmpeg.Args{
Stderr: &stderr, Stderr: &stderr,
Args: args, Args: args,
Config: func(modcfg wazero.ModuleConfig) wazero.ModuleConfig { Config: func(modcfg wazero.ModuleConfig) wazero.ModuleConfig {
fscfg := wazero.NewFSConfig() // needs /dev/urandom fscfg := wazero.NewFSConfig()
fscfg = fscfg.WithReadOnlyDirMount("/dev", "/dev")
fscfg = fscfg.WithDirMount(dirpath, dirpath) // Needs read-only access to
modcfg = modcfg.WithFSConfig(fscfg) // /dev/urandom for some types.
return modcfg urandom := &allowFiles{
{
abs: "/dev/urandom",
flag: os.O_RDONLY,
perm: 0,
},
}
fscfg = fscfg.WithFSMount(urandom, "/dev")
// In+out dirs are always the same (tmp),
// so we can share one file system for
// both + grant different perms to inpath
// (read only) and outpath (read+write).
shared := &allowFiles{
{
abs: inpath,
flag: os.O_RDONLY,
perm: 0,
},
{
abs: outpath,
flag: os.O_RDWR | os.O_CREATE | os.O_TRUNC,
perm: 0666,
},
}
fscfg = fscfg.WithFSMount(shared, path.Dir(inpath))
return modcfg.WithFSConfig(fscfg)
}, },
}) })
if err != nil { if err != nil {
@ -183,9 +202,6 @@ func ffmpeg(ctx context.Context, dirpath string, args ...string) error {
func ffprobe(ctx context.Context, filepath string) (*result, error) { func ffprobe(ctx context.Context, filepath string) (*result, error) {
var stdout byteutil.Buffer var stdout byteutil.Buffer
// Get directory from filepath.
dirpath := path.Dir(filepath)
// Run ffprobe on our given file at path. // Run ffprobe on our given file at path.
_, err := _ffmpeg.Ffprobe(ctx, _ffmpeg.Args{ _, err := _ffmpeg.Ffprobe(ctx, _ffmpeg.Args{
Stdout: &stdout, Stdout: &stdout,
@ -222,9 +238,19 @@ func ffprobe(ctx context.Context, filepath string) (*result, error) {
Config: func(modcfg wazero.ModuleConfig) wazero.ModuleConfig { Config: func(modcfg wazero.ModuleConfig) wazero.ModuleConfig {
fscfg := wazero.NewFSConfig() fscfg := wazero.NewFSConfig()
fscfg = fscfg.WithReadOnlyDirMount(dirpath, dirpath)
modcfg = modcfg.WithFSConfig(fscfg) // Needs read-only access
return modcfg // to file being probed.
in := &allowFiles{
{
abs: filepath,
flag: os.O_RDONLY,
perm: 0,
},
}
fscfg = fscfg.WithFSMount(in, path.Dir(filepath))
return modcfg.WithFSConfig(fscfg)
}, },
}) })
if err != nil { if err != nil {

View file

@ -22,13 +22,60 @@
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/fs"
"os" "os"
"path"
"codeberg.org/gruf/go-bytesize" "codeberg.org/gruf/go-bytesize"
"codeberg.org/gruf/go-iotools" "codeberg.org/gruf/go-iotools"
"codeberg.org/gruf/go-mimetypes" "codeberg.org/gruf/go-mimetypes"
) )
// file represents one file
// with the given flag and perms.
type file struct {
abs string
flag int
perm os.FileMode
}
// allowFiles implements fs.FS to allow
// access to a specified slice of files.
type allowFiles []file
// Open implements fs.FS.
func (af allowFiles) Open(name string) (fs.File, error) {
for _, file := range af {
var (
abs = file.abs
flag = file.flag
perm = file.perm
)
// Allowed to open file
// at absolute path.
if name == file.abs {
return os.OpenFile(abs, flag, perm)
}
// Check for other valid reads.
thisDir, thisFile := path.Split(file.abs)
// Allowed to read directory itself.
if name == thisDir || name == "." {
return os.OpenFile(thisDir, flag, perm)
}
// Allowed to read file
// itself (at relative path).
if name == thisFile {
return os.OpenFile(abs, flag, perm)
}
}
return nil, os.ErrPermission
}
// getExtension splits file extension from path. // getExtension splits file extension from path.
func getExtension(path string) string { func getExtension(path string) string {
for i := len(path) - 1; i >= 0 && path[i] != '/'; i-- { for i := len(path) - 1; i >= 0 && path[i] != '/'; i-- {