Skip to content

Commit

Permalink
fix: scaffolding now correctly works with symlinks (#496)
Browse files Browse the repository at this point in the history
Previously fs.FS was used, which does not support symlinks. Now instead
we return a zip.Reader directly.
  • Loading branch information
alecthomas authored Oct 17, 2023
1 parent 1da072a commit 8b10afb
Show file tree
Hide file tree
Showing 4 changed files with 193 additions and 8 deletions.
31 changes: 28 additions & 3 deletions go-runtime/devel.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,45 @@
package goruntime

import (
"io/fs"
"archive/zip"
"os"
"os/exec"
"path/filepath"
"strings"

"github.com/TBD54566975/ftl/internal"
)

// Files is the FTL Go runtime scaffolding files.
var Files = func() fs.FS {
var Files = func() *zip.Reader {
cmd := exec.Command("git", "rev-parse", "--show-toplevel")
out, err := cmd.CombinedOutput()
if err != nil {
panic(err)
}
dir := filepath.Join(strings.TrimSpace(string(out)), "go-runtime", "scaffolding")
return os.DirFS(dir)
w, err := os.CreateTemp("", "")
if err != nil {
panic(err)
}
defer os.Remove(w.Name()) // This is okay because the zip.Reader will keep it open.
if err != nil {
panic(err)
}

err = internal.ZipDir(dir, w.Name())
if err != nil {
panic(err)
}

info, err := w.Stat()
if err != nil {
panic(err)
}
_, _ = w.Seek(0, 0)
zr, err := zip.NewReader(w, info.Size())
if err != nil {
panic(err)
}
return zr
}()
3 changes: 1 addition & 2 deletions go-runtime/release.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"archive/zip"
"bytes"
_ "embed"
"io/fs"
)

//go:embed scaffolding.zip
Expand All @@ -16,7 +15,7 @@ var archive []byte
//
// scaffolding.zip can be generated by running `bit go-runtime/scaffolding.zip`
// or indirectly via `bit build/release/ftl`.
var Files fs.FS = func() fs.FS {
var Files = func() *zip.Reader {
zr, err := zip.NewReader(bytes.NewReader(archive), int64(len(archive)))
if err != nil {
panic(err)
Expand Down
6 changes: 3 additions & 3 deletions internal/scaffolder.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package internal

import (
"archive/zip"
"io/fs"
"os"
"path/filepath"
Expand All @@ -9,7 +10,6 @@ import (

"github.com/alecthomas/errors"
"github.com/iancoleman/strcase"
"github.com/otiai10/copy"
)

// Scaffold copies the scaffolding files from the given source to the given
Expand All @@ -19,8 +19,8 @@ import (
//
// The functions "snake", "camel", "lowerCamel", "kebab", "upper", and "lower"
// are available.
func Scaffold(source fs.FS, destination string, ctx any) error {
err := copy.Copy(".", destination, copy.Options{FS: source, PermissionControl: copy.AddPermission(0600)})
func Scaffold(source *zip.Reader, destination string, ctx any) error {
err := UnzipDir(source, destination)
if err != nil {
return errors.WithStack(err)
}
Expand Down
161 changes: 161 additions & 0 deletions internal/zip.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
package internal

import (
"archive/zip"
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"strings"

"github.com/alecthomas/errors"
)

// UnzipDir unzips a ZIP archive into the specified directory.
func UnzipDir(zipReader *zip.Reader, destDir string) error {
err := os.MkdirAll(destDir, 0700)
if err != nil {
return errors.WithStack(err)
}
for _, file := range zipReader.File {
destPath, err := sanitizeArchivePath(destDir, file.Name)
if err != nil {
return errors.WithStack(err)
}

// Create directory if it doesn't exist
if file.FileInfo().IsDir() {
err := os.MkdirAll(destPath, file.Mode())
if err != nil {
return errors.WithStack(err)
}
continue
}

// Handle symlinks
if file.Mode()&os.ModeSymlink != 0 {
reader, err := file.Open()
if err != nil {
return errors.WithStack(err)
}
buf := &bytes.Buffer{}
_, err = io.Copy(buf, reader) //nolint:gosec
if err != nil {
return errors.WithStack(err)
}
// This is probably a little bit aggressive, in that the symlink can
// only be beneath its parent directory, rather than the root of the
// zip file. But it's good enough for now.
symlinkDir := filepath.Dir(destPath)
symlinkPath, err := sanitizeArchivePath(symlinkDir, buf.String())
if err != nil {
return errors.WithStack(err)
}
symlinkPath = strings.TrimPrefix(symlinkPath, symlinkDir+"/")
err = os.Symlink(symlinkPath, destPath)
if err != nil {
return errors.WithStack(err)
}
continue
}

// Handle regular files
fileReader, err := file.Open()
if err != nil {
return errors.WithStack(err)
}
defer fileReader.Close()

destFile, err := os.OpenFile(destPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode())
if err != nil {
return errors.WithStack(err)
}
defer destFile.Close()

_, err = io.Copy(destFile, fileReader) //nolint:gosec
if err != nil {
return errors.WithStack(err)
}
}
return nil
}

func ZipDir(srcDir, destZipFile string) error {
zipFile, err := os.Create(destZipFile)
if err != nil {
return errors.WithStack(err)
}
defer zipFile.Close()

zipWriter := zip.NewWriter(zipFile)
defer zipWriter.Close()

return errors.WithStack(filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return errors.WithStack(err)
}

// Determine path for the zip file header
headerPath := strings.TrimPrefix(path, srcDir)
if strings.HasPrefix(headerPath, string(filepath.Separator)) {
headerPath = headerPath[1:]
}

// Add trailing slash to directory paths
if info.IsDir() {
headerPath += "/"
}

header, err := zip.FileInfoHeader(info)
if err != nil {
return errors.WithStack(err)
}
header.Name = headerPath

// Handle symlink
if info.Mode()&os.ModeSymlink != 0 {
dest, err := os.Readlink(path)
if err != nil {
return errors.WithStack(err)
}

header.Method = zip.Store
writer, err := zipWriter.CreateHeader(header)
if err != nil {
return errors.WithStack(err)
}
_, err = writer.Write([]byte(dest))
return errors.WithStack(err)
}

// Handle regular files and directories
writer, err := zipWriter.CreateHeader(header)
if err != nil {
return errors.WithStack(err)
}

if !info.IsDir() {
file, err := os.Open(path)
if err != nil {
return errors.WithStack(err)
}
defer file.Close()

_, err = io.Copy(writer, file)
return errors.WithStack(err)
}

return nil
}))
}

// Sanitize archive file pathing from "G305: Zip Slip vulnerability"
func sanitizeArchivePath(d, t string) (v string, err error) {
v = filepath.Join(d, t)
if strings.HasPrefix(v, filepath.Clean(d)) {
return v, nil
}

return "", fmt.Errorf("%s: %s", "content filepath is tainted", t)
}

0 comments on commit 8b10afb

Please sign in to comment.