Zip Slip in Echo Go with Firestore
Zip Slip in Echo Go with Firestore — how this specific combination creates or exposes the vulnerability
A Zip Slip vulnerability occurs when an archive extraction uses user-supplied paths without proper sanitization, enabling path traversal (e.g., ../../../etc/passwd) to escape the intended extraction directory. In an Echo Go service that integrates with Firestore, this risk can appear in two places: (1) server-side extraction of uploaded archives before storing documents or files in Firestore, and (2) client-side archive generation built by the API where the API dynamically creates ZIP archives using paths derived from Firestore document fields or IDs.
If an Echo Go handler accepts an archive upload, extracts it with a library such as github.com/mholt/archiver/v3, and uses values from Firestore (e.g., a document ID or a user-provided metadata field) to name files inside the archive, untrusted input can traverse directories during extraction. For example, a document field exportFileName stored in Firestore might be used to name a file inside a ZIP built by the API; if that field contains ../../malicious.sh, the generated archive becomes a weapon for Zip Slip. Conversely, when the API serves downloads by extracting user-provided archives and writing files to disk under a Firestore-managed directory, unsanitized archive entries can overwrite arbitrary files outside the target folder, potentially affecting Firestore-side metadata or related resources if the runtime uses predictable paths derived from document IDs.
In this stack, the typical chain is: a client uploads an archive to an Echo Go endpoint; the handler reads Firestore to determine context (project ID, user ID, or output naming); the handler extracts or builds a ZIP using a combination of Firestore metadata and user input; and improper path validation allows directory traversal. Because the vulnerability is a construction/usage issue rather than a Firestore or Echo bug, remediation must focus on path sanitization and strict allowlisting when combining Firestore-derived values with filesystem paths or archive entries.
Concrete example (unsafe):
// Unsafe: using Firestore document field directly in archive path
func unsafeExportHandler(c echo.Context) error {
docID := c.Param("docID")
ctx := context.Background()
client, err := firestore.NewClient(ctx, "my-project")
if err != nil { return err }
defer client.Close()
doc, err := client.Collection("exports").Doc(docID).Get(ctx)
if err != nil { return err }
filename, _ := doc.Data()["fileName"].(string) // user-controlled
// Build a zip using filename from Firestore without validation
buf := new(bytes.Buffer)
z := zip.NewWriter(buf)
f, _ := z.File.Create(filename) // Zip Slip possible
io.WriteString(f, "content")
z.Close()
return c.Blob(http.StatusOK, "application/zip", buf.Bytes())
}
In this pattern, filename pulled from a Firestore document can contain path traversal sequences, and creating a file entry with z.File.Create will honor those paths inside the archive, enabling Zip Slip. An attacker could craft a Firestore document with fileName: "../../secrets.txt" and cause extraction routines elsewhere to escape intended directories.
Firestore-Specific Remediation in Echo Go — concrete code fixes
Remediation focuses on sanitizing any path derived from Firestore before using it in filesystem operations or archive creation. Always validate and normalize paths, use a secure base directory, and prefer generating safe names rather than trusting stored values.
1) Validate and clean filenames before archive creation:
import (
"archive/zip"
"io"
"net/http"
"path/filepath"
"strings"
"github.com/labstack/echo/v4"
"google.golang.org/api/option"
firestore "cloud.google.com/go/firestore"
)
func safeExportHandler(c echo.Context) error {
docID := c.Param("docID")
ctx := context.Background()
client, err := firestore.NewClient(ctx, "my-project", option.WithoutAuthentication())
if err != nil { return err }
defer client.Close()
doc, err := client.Collection("exports").Doc(docID).Get(ctx)
if err != nil { return err }
raw, ok := doc.Data()["fileName"].(string)
if !ok || raw == "" {
return c.String(http.StatusBadRequest, "missing fileName")
}
// Normalize and restrict to a safe basename
clean := filepath.Base(strings.TrimSpace(raw))
if clean == "." || clean == ".." || clean == "" {
return c.String(http.StatusBadRequest, "invalid fileName")
}
buf := new(bytes.Buffer)
z := zip.NewWriter(buf)
f, err := z.File.Create(clean) // safe: clean basename
if err != nil { return err }
_, err = io.WriteString(f, "content")
if err != nil { return err }
err = z.Close()
if err != nil { return err }
return c.Blob(http.StatusOK, "application/zip", buf.Bytes())
}
2) When extracting archives destined for Firestore-related directories, enforce a strict output directory and reject paths that escape it:
import (
"archive/zip"
"io"
"os"
"path/filepath"
)
const baseDir = "/safe/extract"
func extractAndStore(zipData []byte) error {
r, err := zip.OpenReader("", bytes.NewReader(zipData))
if err != nil { return err }
defer r.Close()
for _, f := range r.File {
// Clean and ensure within baseDir
destPath := filepath.Join(baseDir, filepath.Clean(f.Name))
if !strings.HasPrefix(destPath, filepath.Clean(baseDir)+string(os.PathSeparator)) && destPath != filepath.Clean(baseDir) {
return fmt.Errorf("zip entry path traversal blocked: %s", f.Name)
}
// Proceed to extract or to store content in Firestore using a sanitized name
// For example, store f.Name in Firestore only after validation
}
return nil
}
3) Use allowlists for Firestore-derived identifiers used in filenames or keys. If filenames are not required to be user-readable, replace them with UUIDs or Firestore document IDs and store the original name as metadata only.
func storeWithSafeKey(c echo.Context) error {
docID := c.Param("docID")
ctx := context.Background()
client, err := firestore.NewClient(ctx, "my-project")
if err != nil { return err }
defer client.Close()
_, _, err = client.Collection("exports").Doc(docID).Create(ctx, map[string]interface{}{
"safeKey": uuid.NewString(), // store generated key
"userName": "alice",
})
return err
}