diff --git a/README.md b/README.md index 6aff7e6..2ace871 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,9 @@ This project focuses on providing a high-performance, secure, and extensible Git - [x] **Programmable**: Extensible logic using JavaScript (Goja). - [x] **Reverse Proxy**: Support for proxying requests to backends. +> [!WARNING] +> **Security Note**: This project is designed for self-hosted/private environments. It does not perform domain ownership verification for CNAME aliases. In a multi-user environment, users could potentially "hijack" domains by claiming them in their `.pages.yaml`. + ## Get Started diff --git a/README_zh.md b/README_zh.md index dbe2de8..1dd4201 100644 --- a/README_zh.md +++ b/README_zh.md @@ -18,6 +18,9 @@ - [x] **可编程**: 使用 JavaScript (Goja) 编写自定义路由逻辑。 - [x] **反向代理**: 支持将请求代理到后端服务。 +> [!WARNING] +> **安全提示**: 本项目设计用于自托管或私有环境。它不对 CNAME 别名进行域名所有权验证。在多用户环境中,用户可能会通过在 `.pages.yaml` 中声明他人域名来实施“劫持”。 + ## Get Started diff --git a/go.mod b/go.mod index 65c2dfa..9d5a770 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.11.1 go.uber.org/zap v1.27.1 - gopkg.d7z.net/middleware v0.0.0-20260131134426-cea18952b028 + gopkg.d7z.net/middleware v0.0.0-20260131162733-c737c5341584 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/go.sum b/go.sum index 3be44e7..284d989 100644 --- a/go.sum +++ b/go.sum @@ -168,6 +168,8 @@ gopkg.d7z.net/middleware v0.0.0-20260131122058-3c200930af2d h1:nNXZgc02tab2+WuAn gopkg.d7z.net/middleware v0.0.0-20260131122058-3c200930af2d/go.mod h1:TDqvtfgaXzOvm9gbG8t5FF0AKSKve8pcE9uBVix+1pU= gopkg.d7z.net/middleware v0.0.0-20260131134426-cea18952b028 h1:BPm7q2ys8IPHAQe01HBSkYH+2itXuP6DvVPZlg45tM4= gopkg.d7z.net/middleware v0.0.0-20260131134426-cea18952b028/go.mod h1:TDqvtfgaXzOvm9gbG8t5FF0AKSKve8pcE9uBVix+1pU= +gopkg.d7z.net/middleware v0.0.0-20260131162733-c737c5341584 h1:Bdtk/GJQELmfVeoAEyCAUUzR90ZOx6qGA/M3Yjf09Ao= +gopkg.d7z.net/middleware v0.0.0-20260131162733-c737c5341584/go.mod h1:TDqvtfgaXzOvm9gbG8t5FF0AKSKve8pcE9uBVix+1pU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/pkg/core/alias.go b/pkg/core/alias.go index 17fcce3..5b7f117 100644 --- a/pkg/core/alias.go +++ b/pkg/core/alias.go @@ -35,16 +35,50 @@ func (a *DomainAlias) Query(ctx context.Context, domain string) (*Alias, error) } func (a *DomainAlias) Bind(ctx context.Context, domains []string, owner, repo string) error { - oldDomains := make([]string, 0) rKey := base64.URLEncoding.EncodeToString([]byte(fmt.Sprintf("%s/%s", owner, repo))) - if oldStr, err := a.config.Get(ctx, rKey); err == nil { - _ = json.Unmarshal([]byte(oldStr), &oldDomains) - } - for _, oldDomain := range oldDomains { - if err := a.Unbind(ctx, oldDomain); err != nil { + + var oldDomains []string + domainsRaw, _ := json.Marshal(domains) + + for { + success, err := a.config.PutIfNotExists(ctx, rKey, string(domainsRaw), kv.TTLKeep) + if err != nil { return err } + if success { + oldDomains = []string{} + break + } + + oldStr, err := a.config.Get(ctx, rKey) + if err != nil { + continue + } + + if err = json.Unmarshal([]byte(oldStr), &oldDomains); err != nil { + oldDomains = []string{} + } + + success, err = a.config.CompareAndSwap(ctx, rKey, oldStr, string(domainsRaw)) + if err != nil { + return err + } + if success { + break + } } + + newDomainsMap := make(map[string]bool) + for _, d := range domains { + newDomainsMap[d] = true + } + + for _, oldDomain := range oldDomains { + if !newDomainsMap[oldDomain] { + _ = a.Unbind(ctx, oldDomain) + } + } + if len(domains) == 0 { return nil } @@ -53,12 +87,8 @@ func (a *DomainAlias) Bind(ctx context.Context, domains []string, owner, repo st Repo: repo, } aliasMetaRaw, _ := json.Marshal(aliasMeta) - domainsRaw, _ := json.Marshal(domains) - _ = a.config.Put(ctx, rKey, string(domainsRaw), kv.TTLKeep) for _, domain := range domains { - if err := a.config.Put(ctx, domain, string(aliasMetaRaw), kv.TTLKeep); err != nil { - return err - } + _ = a.config.Put(ctx, domain, string(aliasMetaRaw), kv.TTLKeep) } return nil } diff --git a/pkg/core/meta.go b/pkg/core/meta.go index 9513c99..3504bc3 100644 --- a/pkg/core/meta.go +++ b/pkg/core/meta.go @@ -201,7 +201,7 @@ func (s *ServerMeta) parsePageConfig(ctx context.Context, meta *PageMetaContent, cname, err := vfs.ReadString(ctx, "CNAME") if cname != "" && err == nil { cname = strings.TrimSpace(cname) - if al, ok := s.aliasCheck(cname); ok { + if al, ok := s.AliasCheck(cname); ok { alias = append(alias, al) } else { return fmt.Errorf("invalid alias %s", cname) @@ -233,7 +233,7 @@ func (s *ServerMeta) parsePageConfig(ctx context.Context, meta *PageMetaContent, if item == "" { continue } - if al, ok := s.aliasCheck(item); ok { + if al, ok := s.AliasCheck(item); ok { alias = append(alias, al) } else { return fmt.Errorf("invalid alias %s", item) @@ -269,9 +269,9 @@ func (s *ServerMeta) parsePageConfig(ctx context.Context, meta *PageMetaContent, return nil } -var regexpHostname = regexp.MustCompile(`^(?:([a-z0-9-]+|\*)\.)?([a-z0-9-]{1,61})\.([a-z0-9]{2,7})$`) +var regexpHostname = regexp.MustCompile(`^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,18}$`) -func (s *ServerMeta) aliasCheck(cname string) (string, bool) { +func (s *ServerMeta) AliasCheck(cname string) (string, bool) { cname = strings.TrimSpace(cname) if !regexpHostname.MatchString(cname) { return "", false diff --git a/tests/cname_test.go b/tests/cname_test.go new file mode 100644 index 0000000..86e3024 --- /dev/null +++ b/tests/cname_test.go @@ -0,0 +1,69 @@ +package tests + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "gopkg.d7z.net/gitea-pages/pkg/core" + "gopkg.d7z.net/middleware/kv" +) + +func TestCNAMERegex(t *testing.T) { + db, _ := kv.NewMemory("") + meta := core.NewServerMeta(nil, nil, "example.com", nil, db, 0, 0, 0) + + tests := []struct { + domain string + valid bool + }{ + {"a.com", true}, + {"sub.a.com", true}, + {"a.b.c.d.com", true}, + {"invalid_name.com", false}, + {"-start.com", false}, + {"end-.com", false}, + {"a.com-too-long-tld-xxxxxxxxxxxxxxxxxxx", false}, + } + + for _, tt := range tests { + _, ok := meta.AliasCheck(tt.domain) + assert.Equal(t, tt.valid, ok, "Testing domain: %s", tt.domain) + } +} + +func TestAliasBindCAS(t *testing.T) { + db, _ := kv.NewMemory("") + + alias := core.NewDomainAlias(db) + + ctx := context.Background() + + err := alias.Bind(ctx, []string{"a.com", "b.com"}, "owner1", "repo1") + + assert.NoError(t, err) + + a, err := alias.Query(ctx, "a.com") + + assert.NoError(t, err) + + if assert.NotNil(t, a) { + assert.Equal(t, "owner1", a.Owner) + } + + err = alias.Bind(ctx, []string{"b.com", "c.com"}, "owner1", "repo1") + + assert.NoError(t, err) + + _, err = alias.Query(ctx, "a.com") + + assert.Error(t, err) + + a, err = alias.Query(ctx, "c.com") + + assert.NoError(t, err) + + if assert.NotNil(t, a) { + assert.Equal(t, "owner1", a.Owner) + } +}