fix: contain generated files and assets and harden SQL rendering

This commit is contained in:
2026-10-12 20:59:46 +02:00
parent 2fa48335d3
commit dd97413b8e
68 changed files with 2328 additions and 798 deletions
+38 -30
View File
@@ -18,9 +18,10 @@ import (
"regexp"
"sort"
"strconv"
"strings"
"gopkg.in/yaml.v3"
"git.warky.dev/wdevs/relspecgo/pkg/safefile"
)
// ManifestEntry describes a single file to load from an assets.yaml manifest.
@@ -37,6 +38,9 @@ type ManifestEntry struct {
// Item combines a manifest entry with its ordering metadata and the resolved
// directory where the manifest and asset file reside.
type Item struct {
// AssetRoot is the trusted scan root, retained for validation at execution time.
AssetRoot string
MaxFileBytes int64
// Priority and Sequence come from the parent directory's naming pattern.
Priority int
Sequence uint
@@ -57,7 +61,12 @@ var dirPattern = regexp.MustCompile(`^(\d+)[_-](\d+)[_-](.+)$`)
// absent or contains invalid YAML.
func LoadManifest(dir string) ([]ManifestEntry, error) {
manifestPath := filepath.Join(dir, "assets.yaml")
data, err := os.ReadFile(manifestPath)
root, err := openAssetRoot(dir)
if err != nil {
return nil, err
}
defer root.Close()
data, err := safefile.Read(root, "assets.yaml", DefaultMaxAssetBytes)
if err != nil {
return nil, fmt.Errorf("reading %s: %w", manifestPath, err)
}
@@ -92,7 +101,7 @@ func ScanDir(baseDir string) ([]Item, error) {
if d.IsDir() {
return nil
}
if d.Name() != "assets.yaml" {
if d.Type()&os.ModeSymlink != 0 || d.Name() != "assets.yaml" {
return nil
}
@@ -119,10 +128,12 @@ func ScanDir(baseDir string) ([]Item, error) {
}
items = append(items, Item{
Priority: priority,
Sequence: sequence,
DirName: dirName,
Dir: manifestDir,
AssetRoot: absBase,
MaxFileBytes: DefaultMaxAssetBytes,
Priority: priority,
Sequence: sequence,
DirName: dirName,
Dir: manifestDir,
Entry: ManifestEntry{
File: resolved, // absolute path, safe to read
Call: entry.Call,
@@ -170,31 +181,28 @@ func parseDirName(dir string) (priority int, sequence uint, name string) {
// Returns the absolute path, a skip flag (true when the entry should be silently
// dropped), and any hard error.
func resolveAssetPath(absBase, manifestDir, file string) (absPath string, skip bool, err error) {
// Clean and join before any symlink resolution so we can detect traversal.
joined := filepath.Join(manifestDir, filepath.Clean(file))
// Ensure the cleaned path is still inside absBase.
rel, err := filepath.Rel(absBase, joined)
if err != nil || strings.HasPrefix(rel, "..") {
// Path escapes the base directory; skip silently.
if _, err := safefile.Relative(file); err != nil {
return "", true, nil
}
// Walk each component to detect symlinks.
parts := strings.Split(rel, string(filepath.Separator))
current := absBase
for _, part := range parts {
current = filepath.Join(current, part)
info, statErr := os.Lstat(current)
if statErr != nil {
// File doesn't exist; skip.
return "", true, nil
}
if info.Mode()&os.ModeSymlink != 0 {
// Symlink in path; skip silently.
return "", true, nil
}
joined := filepath.Join(manifestDir, file)
rel, err := filepath.Rel(absBase, joined)
if err != nil {
return "", true, nil
}
root, err := openAssetRoot(absBase)
if err != nil {
return "", false, err
}
defer root.Close()
if err := safefile.Check(root, rel); err != nil {
return "", true, nil
}
info, err := root.Stat(rel)
if err != nil || !info.Mode().IsRegular() {
return "", true, nil
}
if info.Size() > DefaultMaxAssetBytes {
return "", false, fmt.Errorf("asset %q exceeds %d byte limit", rel, DefaultMaxAssetBytes)
}
return joined, false, nil
}