mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2024-11-25 01:41:00 +00:00
[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:
parent
e10aa76612
commit
cd93a5baf3
2 changed files with 104 additions and 31 deletions
|
@ -21,6 +21,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -39,10 +40,7 @@ import (
|
||||||
// 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 {
|
||||||
|
|
|
@ -22,13 +22,60 @@ import (
|
||||||
"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-- {
|
||||||
|
|
Loading…
Reference in a new issue