Skip to content

Commit

Permalink
Add some tests and filename sanitization
Browse files Browse the repository at this point in the history
  • Loading branch information
chriskuehl committed Sep 2, 2024
1 parent b39655a commit 9f7eca6
Show file tree
Hide file tree
Showing 10 changed files with 310 additions and 169 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,4 @@ watch-assets:

.PHONY: test
test:
go test -v ./...
go test ./...
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.23.0

require (
github.com/adrg/xdg v0.5.0
github.com/google/go-cmp v0.6.0
github.com/spf13/cobra v1.8.1
golang.org/x/term v0.23.0
)
Expand Down
20 changes: 4 additions & 16 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,37 +1,25 @@
github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls=
github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E=
github.com/adrg/xdg v0.5.0 h1:dDaZvhMXatArP1NPHhnfaQUqWBLBsmx1h1HXQdMoFCY=
github.com/adrg/xdg v0.5.0/go.mod h1:dDdY4M4DF9Rjy4kHPeNL+ilVF+p2lK8IdM9/rTSGcI4=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU=
golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
24 changes: 12 additions & 12 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,16 @@ import (

type Config struct {
// Site configuration.
StorageBackend storage.Backend
Branding string
CustomFooterHTML template.HTML
AbuseContactEmail string
MaxUploadBytes int64
MaxMultipartMemoryBytes int64
HomeURL url.URL
ObjectURLPattern url.URL
HTMLURLPattern url.URL
DisallowedFileExtensions []string
StorageBackend storage.Backend
Branding string
CustomFooterHTML template.HTML
AbuseContactEmail string
MaxUploadBytes int64
MaxMultipartMemoryBytes int64
HomeURL url.URL
ObjectURLPattern url.URL
HTMLURLPattern url.URL
ForbiddenFileExtensions map[string]struct{}

// Runtime options.
DevMode bool
Expand Down Expand Up @@ -57,9 +57,9 @@ func (c *Config) Validate() []string {
if !strings.Contains(c.HTMLURLPattern.Path, "%s") {
errs = append(errs, "HTMLURLPattern must contain a '%s' placeholder")
}
for _, ext := range c.DisallowedFileExtensions {
for ext := range c.ForbiddenFileExtensions {
if strings.HasPrefix(ext, ".") {
errs = append(errs, "DisallowedFileExtensions should not start with a dot: "+ext)
errs = append(errs, "ForbiddenFileExtensions should not start with a dot: "+ext)
}
}
if c.Version == "" {
Expand Down
22 changes: 5 additions & 17 deletions server/storage/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,22 @@ package storage

import (
"context"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"

"github.com/chriskuehl/fluffy/server/uploads"
)

type Object struct {
Key string
Key uploads.SanitizedKey
Links []string
MetadataURL string
Reader io.Reader
}

func (o Object) Validate() error {
if o.Key == "" {
return errors.New("key must not be empty")
}
if filepath.Clean(o.Key) != o.Key {
return errors.New("key contains invalid characters")
}
return nil
}

type Backend interface {
StoreObject(ctx context.Context, obj Object) error
StoreHTML(ctx context.Context, obj Object) error
Expand All @@ -50,15 +41,12 @@ func absPath(path string) (string, error) {
}

func (b *FilesystemBackend) store(root string, obj Object) error {
if err := obj.Validate(); err != nil {
return fmt.Errorf("validating object: %w", err)
}
realRoot, err := absPath(root)
if err != nil {
return fmt.Errorf("getting real root: %w", err)
}

parentPath, err := absPath(filepath.Join(root, filepath.Dir(obj.Key)))
parentPath, err := absPath(filepath.Join(root, filepath.Dir(obj.Key.String())))
if err != nil {
return fmt.Errorf("getting parent path: %w", err)
}
Expand All @@ -67,7 +55,7 @@ func (b *FilesystemBackend) store(root string, obj Object) error {
return fmt.Errorf("parent path %q is outside of root %q", parentPath, realRoot)
}

path := filepath.Join(parentPath, filepath.Base(obj.Key))
path := filepath.Join(parentPath, filepath.Base(obj.Key.String()))
file, err := os.Create(path)
if err != nil {
return fmt.Errorf("creating file: %w", err)
Expand Down
50 changes: 0 additions & 50 deletions server/uploads.go

This file was deleted.

86 changes: 86 additions & 0 deletions server/uploads/uploads.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package uploads

import (
"crypto/rand"
"fmt"
"math/big"
"path/filepath"
"strings"
)

const (
storedFileNameLength = 32
storedFileNameChars = "bcdfghjklmnpqrstvwxzBCDFGHJKLMNPQRSTVWXZ0123456789"
)

var (
ErrForbiddenExtension = fmt.Errorf("forbidden extension")

// Extensions that traditionally wrap another file extension.
wrapperExtensions = map[string]struct{}{
"bz2": {},
"gz": {},
"xz": {},
"zst": {},
}
)

func genUniqueObjectID() (string, error) {
var s strings.Builder
for i := 0; i < storedFileNameLength; i++ {
r, err := rand.Int(rand.Reader, big.NewInt(int64(len(storedFileNameChars))))
if err != nil {
return "", fmt.Errorf("generating random number: %w", err)
}
if !r.IsInt64() {
return "", fmt.Errorf("random number is not an int64")
}
s.WriteByte(storedFileNameChars[r.Int64()])
}
return s.String(), nil
}

func extractExtension(name string) string {
fullExt := ""
for strings.Contains(name, ".") {
ext := filepath.Ext(name)
name = strings.TrimSuffix(name, ext)
if ext == "." {
// Don't add ".", but keep processing any additional extensions.
continue
}
fullExt = ext + fullExt
if _, ok := wrapperExtensions[strings.TrimPrefix(ext, ".")]; !ok {
return fullExt
}
}
return fullExt
}

type SanitizedKey struct {
UniqueID string
Extension string
}

func (s SanitizedKey) String() string {
return s.UniqueID + s.Extension
}

func SanitizeUploadName(name string, forbiddenExtensions map[string]struct{}) (*SanitizedKey, error) {
name = strings.ReplaceAll(name, string(filepath.Separator), "/")
name = name[strings.LastIndex(name, "/")+1:]
id, err := genUniqueObjectID()
if err != nil {
return nil, fmt.Errorf("generating unique object ID: %w", err)
}
ext := extractExtension(name)
for _, extPart := range strings.Split(ext, ".") {
if _, ok := forbiddenExtensions[extPart]; ok {
return nil, ErrForbiddenExtension
}
}
return &SanitizedKey{
UniqueID: id,
Extension: ext,
}, nil
}
Loading

0 comments on commit 9f7eca6

Please sign in to comment.