Files
caddy/cmd/storagefuncs.go
Matthew HoltandCopilot 3fea1cc800 cmd: validate storage archive entries before import
Reject absolute, non-canonical, traversing or platform-unsafe keys and
non-regular entries (links, devices, FIFOs) before writing to storage,
so importing an untrusted archive cannot write outside the storage root.
Directory entries are skipped and a single leading "./" is removed, so
archives created with tar(1) from a storage directory still import.

Addresses scan report 89de9a51d786c23e.

Code written by GitHub Copilot (GPT-6.1 Sol, Claude Opus 5.5); reviewed by mholt.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-11-11 11:06:40 -07:00

248 lines
6.8 KiB
Go

// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package caddycmd
import (
"archive/tar"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"strings"
"github.com/caddyserver/certmagic"
"github.com/caddyserver/caddy/v2"
)
type storVal struct {
StorageRaw json.RawMessage `json:"storage,omitempty" caddy:"namespace=caddy.storage inline_key=module"`
}
// determineStorage returns the top-level storage module from the given config.
// It may return nil even if no error.
func determineStorage(configFile string, configAdapter string) (*storVal, error) {
cfg, _, _, err := LoadConfig(configFile, configAdapter)
if err != nil {
return nil, err
}
// storage defaults to FileStorage if not explicitly
// defined in the config, so the config can be valid
// json but unmarshaling will fail.
if !json.Valid(cfg) {
return nil, &json.SyntaxError{}
}
var tmpStruct storVal
err = json.Unmarshal(cfg, &tmpStruct)
if err != nil {
// default case, ignore the error
if _, ok := errors.AsType[*json.SyntaxError](err); ok {
return nil, nil
}
return nil, err
}
return &tmpStruct, nil
}
func cmdImportStorage(fl Flags) (int, error) {
importStorageCmdConfigFlag := fl.String("config")
importStorageCmdImportFile := fl.String("input")
if importStorageCmdConfigFlag == "" {
return caddy.ExitCodeFailedStartup, errors.New("--config is required")
}
if importStorageCmdImportFile == "" {
return caddy.ExitCodeFailedStartup, errors.New("--input is required")
}
// extract storage from config if possible
storageCfg, err := determineStorage(importStorageCmdConfigFlag, "")
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
// load specified storage or fallback to default
var stor certmagic.Storage
ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
defer cancel()
if storageCfg != nil && storageCfg.StorageRaw != nil {
val, err := ctx.LoadModule(storageCfg, "StorageRaw")
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
stor, err = val.(caddy.StorageConverter).CertMagicStorage()
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
} else {
stor = caddy.DefaultStorage
}
// setup input
var f *os.File
if importStorageCmdImportFile == "-" {
f = os.Stdin
} else {
f, err = os.Open(importStorageCmdImportFile)
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("opening input file: %v", err)
}
defer f.Close()
}
// store each archive element
tr := tar.NewReader(f)
for {
hdr, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return caddy.ExitCodeFailedQuit, fmt.Errorf("reading archive: %v", err)
}
// storage has no directories; archives made with tar(1) include them
if hdr.Typeflag == tar.TypeDir {
continue
}
key := strings.TrimPrefix(hdr.Name, "./")
if !fs.ValidPath(key) || key == "." || strings.ContainsAny(key, `\:`) {
return caddy.ExitCodeFailedQuit, fmt.Errorf("invalid storage key in archive: %q", hdr.Name)
}
if _, err := filepath.Localize(key); err != nil {
return caddy.ExitCodeFailedQuit, fmt.Errorf("invalid storage key in archive %q: %w", hdr.Name, err)
}
if hdr.Typeflag != tar.TypeReg {
return caddy.ExitCodeFailedQuit, fmt.Errorf("unsupported archive entry type %d for %q", hdr.Typeflag, hdr.Name)
}
b, err := io.ReadAll(tr)
if err != nil {
return caddy.ExitCodeFailedQuit, fmt.Errorf("reading archive: %v", err)
}
err = stor.Store(ctx, key, b)
if err != nil {
return caddy.ExitCodeFailedQuit, fmt.Errorf("storing archive entry %q: %w", hdr.Name, err)
}
}
fmt.Println("Successfully imported storage")
return caddy.ExitCodeSuccess, nil
}
func cmdExportStorage(fl Flags) (int, error) {
exportStorageCmdConfigFlag := fl.String("config")
exportStorageCmdOutputFlag := fl.String("output")
if exportStorageCmdConfigFlag == "" {
return caddy.ExitCodeFailedStartup, errors.New("--config is required")
}
if exportStorageCmdOutputFlag == "" {
return caddy.ExitCodeFailedStartup, errors.New("--output is required")
}
// extract storage from config if possible
storageCfg, err := determineStorage(exportStorageCmdConfigFlag, "")
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
// load specified storage or fallback to default
var stor certmagic.Storage
ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
defer cancel()
if storageCfg != nil && storageCfg.StorageRaw != nil {
val, err := ctx.LoadModule(storageCfg, "StorageRaw")
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
stor, err = val.(caddy.StorageConverter).CertMagicStorage()
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
} else {
stor = caddy.DefaultStorage
}
// enumerate all keys
keys, err := stor.List(ctx, "", true)
if err != nil {
return caddy.ExitCodeFailedStartup, err
}
// setup output
var f *os.File
if exportStorageCmdOutputFlag == "-" {
f = os.Stdout
} else {
f, err = os.Create(exportStorageCmdOutputFlag)
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("opening output file: %v", err)
}
defer f.Close()
}
// `IsTerminal: true` keys hold the values we
// care about, write them out
tw := tar.NewWriter(f)
for _, k := range keys {
info, err := stor.Stat(ctx, k)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
caddy.Log().Warn(fmt.Sprintf("key: %s removed while export is in-progress", k))
continue
}
return caddy.ExitCodeFailedQuit, err
}
if info.IsTerminal {
v, err := stor.Load(ctx, k)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
caddy.Log().Warn(fmt.Sprintf("key: %s removed while export is in-progress", k))
continue
}
return caddy.ExitCodeFailedQuit, err
}
hdr := &tar.Header{
Name: k,
Mode: 0o600,
Size: int64(len(v)),
ModTime: info.Modified,
}
if err = tw.WriteHeader(hdr); err != nil {
return caddy.ExitCodeFailedQuit, fmt.Errorf("writing archive: %v", err)
}
if _, err = tw.Write(v); err != nil {
return caddy.ExitCodeFailedQuit, fmt.Errorf("writing archive: %v", err)
}
}
}
if err = tw.Close(); err != nil {
return caddy.ExitCodeFailedQuit, fmt.Errorf("writing archive: %v", err)
}
return caddy.ExitCodeSuccess, nil
}