Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 47 additions & 7 deletions internal/storage/storage_fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,25 @@
root string
}

func (fs *fsStorage) Stat(key string) (stat Stat, err error) {
// safeJoinPath joins and validates that the resulting path is within fs.root
func (fs *fsStorage) safeJoinPath(key string) (string, error) {
filename := filepath.Join(fs.root, key)
absPath, err := filepath.Abs(filename)
if err != nil {
return "", err
}
// Ensure absPath is within fs.root
if !strings.HasPrefix(absPath, fs.root+string(os.PathSeparator)) && absPath != fs.root {
return "", errors.New("invalid file path")
}
return absPath, nil
}

func (fs *fsStorage) Stat(key string) (stat Stat, err error) {
filename, err := fs.safeJoinPath(key)
if err != nil {
return nil, ErrNotFound
}
fi, err := os.Lstat(filename)
if err != nil {
if os.IsNotExist(err) || strings.HasSuffix(err.Error(), "not a directory") {
Expand All @@ -43,7 +60,11 @@
}

func (fs *fsStorage) Get(key string) (content io.ReadCloser, stat Stat, err error) {
filename := filepath.Join(fs.root, key)
filename, err := fs.safeJoinPath(key)
if err != nil {
err = ErrNotFound
return
}
file, err := os.Open(filename)
if err != nil && (os.IsNotExist(err) || strings.HasSuffix(err.Error(), "not a directory")) {
err = ErrNotFound
Expand All @@ -60,12 +81,23 @@

func (fs *fsStorage) List(prefix string) (keys []string, err error) {
dir := strings.TrimSuffix(utils.NormalizePathname(prefix)[1:], "/")
return findFiles(filepath.Join(fs.root, dir), dir)
absDir, err := fs.safeJoinPath(dir)
if err != nil {
return nil, err
}
return findFiles(absDir, dir)
}

func (fs *fsStorage) Put(key string, content io.Reader) (err error) {
filename := filepath.Join(fs.root, key)
err = ensureDir(filepath.Dir(filename))
filename, err := fs.safeJoinPath(key)
return ErrNotFound
}
dir := filepath.Dir(filename)

Check failure on line 95 in internal/storage/storage_fs.go

View workflow job for this annotation

GitHub Actions / Deno (2.x)

syntax error: non-declaration statement outside function body

Check failure on line 95 in internal/storage/storage_fs.go

View workflow job for this annotation

GitHub Actions / Deno (1.x)

syntax error: non-declaration statement outside function body
if !strings.HasPrefix(dir, fs.root+string(os.PathSeparator)) && dir != fs.root {
return errors.New("invalid file path")
}
err = ensureDir(dir)
if err != nil {
if err != nil {
return
}
Expand All @@ -84,9 +116,17 @@
}

func (fs *fsStorage) Delete(key string) (err error) {
return os.Remove(filepath.Join(fs.root, key))
filename, err := fs.safeJoinPath(key)
if err != nil {
return ErrNotFound
}
return os.Remove(filename)
}

absDir, err := fs.safeJoinPath(dir)

Check failure on line 126 in internal/storage/storage_fs.go

View workflow job for this annotation

GitHub Actions / Deno (2.x)

syntax error: non-declaration statement outside function body

Check failure on line 126 in internal/storage/storage_fs.go

View workflow job for this annotation

GitHub Actions / Deno (1.x)

syntax error: non-declaration statement outside function body
if err != nil {
return nil, ErrNotFound
}
func (fs *fsStorage) DeleteAll(prefix string) (deletedKeys []string, err error) {
dir := strings.TrimSuffix(utils.NormalizePathname(prefix)[1:], "/")
if dir == "" {
Expand All @@ -96,7 +136,7 @@
if err != nil {
return
}
err = os.RemoveAll(filepath.Join(fs.root, dir))
err = os.RemoveAll(absDir)
if err != nil {
return
}
Expand Down
26 changes: 25 additions & 1 deletion server/build_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"os"
"path"
"path/filepath"
"slices"
"sort"
"strings"
Expand Down Expand Up @@ -1315,7 +1316,30 @@ func normalizeImportSpecifier(specifier string) string {

// validateJSFile validates javascript/typescript module from the given file.
func validateJSFile(filename string) (isESM bool, namedExports []string, err error) {
data, err := os.ReadFile(filename)
absFilename, err := filepath.Abs(filename)
if err != nil {
return
}

// Example rootDir for validation:
// NOTE: you may need to pass the package root in as argument or otherwise obtain it.
// This version assumes validateJSFile only accepts files under the current working directory.
rootDir, err := filepath.Abs(".")
if err != nil {
return
}

// enforce trailing separator for proper prefix match
rootDirWithSep := rootDir
if !strings.HasSuffix(rootDirWithSep, string(filepath.Separator)) {
rootDirWithSep += string(filepath.Separator)
}
if !strings.HasPrefix(absFilename, rootDirWithSep) {
err = errors.New("access outside of allowed package directory")
return
}

data, err := os.ReadFile(absFilename)
if err != nil {
return
}
Expand Down
8 changes: 7 additions & 1 deletion server/npmrc.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (
"strings"
"sync"
"time"

"regexp"
"github.com/Masterminds/semver/v3"
"github.com/esm-dev/esm.sh/internal/fetch"
"github.com/esm-dev/esm.sh/internal/jsonc"
Expand Down Expand Up @@ -87,6 +87,12 @@ func NewNpmRcFromJSON(jsonData []byte) (npmrc *NpmRC, err error) {
if err != nil {
return nil, err
}
if rc.zoneId != "" {
zoneIdPattern := regexp.MustCompile(`^[a-zA-Z0-9_\-]+$`)
if !zoneIdPattern.MatchString(rc.zoneId) {
return nil, errors.New("invalid zoneId: must be alphanumeric or _ or - only")
}
}
if rc.Registry == "" {
rc.Registry = config.NpmRegistry
} else if !strings.HasSuffix(rc.Registry, "/") {
Expand Down
12 changes: 11 additions & 1 deletion web/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -896,7 +896,17 @@ func (s *Handler) ServeHmrWS(w http.ResponseWriter, r *http.Request) {
}

func (s *Handler) parseImportMap(filename string) (importMapRaw []byte, importMap importmap.ImportMap, err error) {
file, err := os.Open(filepath.Join(s.config.AppDir, filename))
// Mitigate path traversal: allow only paths within AppDir
absAppDir, err := filepath.Abs(s.config.AppDir)
if err != nil {
return
}
absPath, err := filepath.Abs(filepath.Join(absAppDir, filename))
if err != nil || !strings.HasPrefix(absPath, absAppDir+string(os.PathSeparator)) {
err = errors.New("Invalid file name")
return
}
file, err := os.Open(absPath)
if err != nil {
return
}
Expand Down
Loading