mirror of
https://github.com/go-admin-team/go-admin.git
synced 2026-09-29 04:27:17 +00:00
fix🐛: keep the code generator's writes inside their roots
The generator joins a table's package, table and business names into the paths it writes, and wrote them with os.MkdirAll and os.WriteFile. A name carrying "..", or a symlink under the target directory pointing elsewhere, took the write wherever it led. Every file is now written through os.Root: backend files under the working directory, frontend files under gen.frontpath. A path that resolves outside its root is refused, symlinks included, and the request reports the write failure without writing anything outside.
This commit is contained in:
+38
-21
@@ -319,22 +319,23 @@ func (e Gen) NOActionsGen(c *gin.Context, tab tools.SysTables) bool {
|
||||
// up - a flat gen/{BusinessName}.ts would let two tables in different
|
||||
// packages silently overwrite each other's translations, since
|
||||
// BusinessName only has a pattern check, no uniqueness check.
|
||||
back, front := ".", config.GenConfig.FrontPath
|
||||
files := []struct {
|
||||
path string
|
||||
content []byte
|
||||
root, name string
|
||||
content []byte
|
||||
}{
|
||||
{"./app/" + tab.PackageName + "/models/" + tab.TBName + ".go", out[0]},
|
||||
{"./app/" + tab.PackageName + "/apis/" + tab.TBName + ".go", out[1]},
|
||||
{"./app/" + tab.PackageName + "/router/" + tab.TBName + ".go", out[2]},
|
||||
{config.GenConfig.FrontPath + "/api/" + tab.PackageName + "/" + tab.MLTBName + ".ts", out[3]},
|
||||
{config.GenConfig.FrontPath + "/views/" + tab.PackageName + "/" + tab.MLTBName + "/index.vue", out[4]},
|
||||
{"./app/" + tab.PackageName + "/service/dto/" + tab.TBName + ".go", out[5]},
|
||||
{"./app/" + tab.PackageName + "/service/" + tab.TBName + ".go", out[6]},
|
||||
{config.GenConfig.FrontPath + "/lang/zh-CN/gen/" + tab.PackageName + "/" + tab.BusinessName + ".ts", out[7]},
|
||||
{config.GenConfig.FrontPath + "/lang/en-US/gen/" + tab.PackageName + "/" + tab.BusinessName + ".ts", out[8]},
|
||||
{back, "app/" + tab.PackageName + "/models/" + tab.TBName + ".go", out[0]},
|
||||
{back, "app/" + tab.PackageName + "/apis/" + tab.TBName + ".go", out[1]},
|
||||
{back, "app/" + tab.PackageName + "/router/" + tab.TBName + ".go", out[2]},
|
||||
{front, "api/" + tab.PackageName + "/" + tab.MLTBName + ".ts", out[3]},
|
||||
{front, "views/" + tab.PackageName + "/" + tab.MLTBName + "/index.vue", out[4]},
|
||||
{back, "app/" + tab.PackageName + "/service/dto/" + tab.TBName + ".go", out[5]},
|
||||
{back, "app/" + tab.PackageName + "/service/" + tab.TBName + ".go", out[6]},
|
||||
{front, "lang/zh-CN/gen/" + tab.PackageName + "/" + tab.BusinessName + ".ts", out[7]},
|
||||
{front, "lang/en-US/gen/" + tab.PackageName + "/" + tab.BusinessName + ".ts", out[8]},
|
||||
}
|
||||
for _, f := range files {
|
||||
if err := writeGenerated(f.path, f.content); err != nil {
|
||||
if err := writeGenerated(f.root, f.name, f.content); err != nil {
|
||||
log.Error(err)
|
||||
e.Error(500, err, fmt.Sprintf("生成文件写入失败!错误详情:%s", err.Error()))
|
||||
return false
|
||||
@@ -373,7 +374,7 @@ func (e Gen) genApiToFile(c *gin.Context, tab tools.SysTables) bool {
|
||||
e.Error(500, err, fmt.Sprintf("数据迁移模版渲染失败!错误详情:%s", err.Error()))
|
||||
return false
|
||||
}
|
||||
if err := writeGenerated("./cmd/migrate/migration/version-local/"+i+"_migrate.go", out[0]); err != nil {
|
||||
if err := writeGenerated(".", "cmd/migrate/migration/version-local/"+i+"_migrate.go", out[0]); err != nil {
|
||||
e.Logger.Error(err)
|
||||
e.Error(500, err, fmt.Sprintf("数据迁移文件写入失败!错误详情:%s", err.Error()))
|
||||
return false
|
||||
@@ -554,14 +555,30 @@ func renderAll(data any, tpls ...*template.Template) ([][]byte, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// writeGenerated writes one generated file, creating its directory first,
|
||||
// which pkg.FileCreate does not do. It replaced pkg.FileCreate here while
|
||||
// that returned no error and, when the file could not be created, ended the
|
||||
// process with log.Fatalln - one unwritable path took the whole server down
|
||||
// with it. go-admin-core v2.11.0 made pkg.FileCreate return the error.
|
||||
func writeGenerated(path string, content []byte) error {
|
||||
if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
|
||||
// writeGenerated writes content to name, a slash-separated path under root,
|
||||
// creating root and the directories in between.
|
||||
//
|
||||
// It goes through os.Root, so name cannot leave root: a ".." component, an
|
||||
// absolute path, or a symlink under root that points outside it is refused
|
||||
// rather than followed. root itself is configuration (the working directory,
|
||||
// or gen.frontpath), and may be or contain a symlink.
|
||||
//
|
||||
// It replaced pkg.FileCreate here while that returned no error and, when the
|
||||
// file could not be created, ended the process with log.Fatalln - one
|
||||
// unwritable path took the whole server down with it. go-admin-core v2.11.0
|
||||
// made pkg.FileCreate return the error.
|
||||
func writeGenerated(root, name string, content []byte) error {
|
||||
if err := os.MkdirAll(root, os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(path, content, 0o644)
|
||||
r, err := os.OpenRoot(root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Close()
|
||||
name = filepath.FromSlash(name)
|
||||
if err := r.MkdirAll(filepath.Dir(name), os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
return r.WriteFile(name, content, 0o644)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// The generator builds the paths it writes to from three fields of a table's
|
||||
// configuration. These tests hold that none of them can take a write outside
|
||||
// the working directory or gen.frontpath, and that a refused write leaves
|
||||
// nothing behind - inside the roots or outside them.
|
||||
|
||||
// filesUnder lists every file below dir, or none when dir does not exist.
|
||||
func filesUnder(t *testing.T, dir string) []string {
|
||||
t.Helper()
|
||||
var files []string
|
||||
_ = filepath.WalkDir(dir, func(p string, d fs.DirEntry, err error) error {
|
||||
if err == nil && !d.IsDir() {
|
||||
files = append(files, p)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return files
|
||||
}
|
||||
|
||||
func TestNOActionsGenWritesEveryFileInsideItsRoots(t *testing.T) {
|
||||
dir := genWorkspace(t)
|
||||
|
||||
var ok bool
|
||||
bodies := runGen(t, nil, func(c *gin.Context) { ok = Gen{}.NOActionsGen(c, genTable()) }, nil)
|
||||
if !ok || len(bodies) != 0 {
|
||||
t.Fatalf("ok=%v, responses %+v; want true and no error response", ok, bodies)
|
||||
}
|
||||
want := []string{
|
||||
"app/admin/models/gen_err.go",
|
||||
"app/admin/apis/gen_err.go",
|
||||
"app/admin/router/gen_err.go",
|
||||
"app/admin/service/dto/gen_err.go",
|
||||
"app/admin/service/gen_err.go",
|
||||
"ui/api/admin/gen-err.ts",
|
||||
"ui/views/admin/gen-err/index.vue",
|
||||
"ui/lang/zh-CN/gen/admin/genErr.ts",
|
||||
"ui/lang/en-US/gen/admin/genErr.ts",
|
||||
}
|
||||
for _, rel := range want {
|
||||
if _, err := os.Stat(filepath.Join(dir, filepath.FromSlash(rel))); err != nil {
|
||||
t.Errorf("%s was not written: %v", rel, err)
|
||||
}
|
||||
}
|
||||
if got := len(writtenFiles(t, dir)); got != len(want) {
|
||||
t.Errorf("wrote %d files, want %d: %v", got, len(want), writtenFiles(t, dir))
|
||||
}
|
||||
}
|
||||
|
||||
// A symlink under a root that points out of it is the case a name check
|
||||
// cannot see: every component of the name is legal. os.Root refuses to follow
|
||||
// it. The file planted where the write would land must survive unchanged.
|
||||
func TestNOActionsGenDoesNotFollowASymlinkOutOfItsRoot(t *testing.T) {
|
||||
for _, link := range []string{"app/admin", "ui/api"} {
|
||||
t.Run(link, func(t *testing.T) {
|
||||
dir := genWorkspace(t)
|
||||
outside := t.TempDir()
|
||||
planted := filepath.Join(outside, "models", "gen_err.go")
|
||||
if err := os.MkdirAll(filepath.Dir(planted), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(planted, []byte("original"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
at := filepath.Join(dir, filepath.FromSlash(link))
|
||||
if err := os.MkdirAll(filepath.Dir(at), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Symlink(outside, at); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var ok bool
|
||||
bodies := runGen(t, nil, func(c *gin.Context) { ok = Gen{}.NOActionsGen(c, genTable()) }, nil)
|
||||
|
||||
if ok || len(bodies) != 1 || bodies[0].Code != 500 || !strings.Contains(bodies[0].Msg, "生成文件写入失败") {
|
||||
t.Fatalf("ok=%v, responses %+v; want false and one 500 for the write", ok, bodies)
|
||||
}
|
||||
if b, err := os.ReadFile(planted); err != nil || string(b) != "original" {
|
||||
t.Errorf("the file outside the root reads %q (err %v), want it untouched", b, err)
|
||||
}
|
||||
if files := filesUnder(t, outside); len(files) != 1 {
|
||||
t.Errorf("files outside the root: %v, want only the planted one", files)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user