135 lines
4.4 KiB
Go
135 lines
4.4 KiB
Go
package nginxactivation
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"syscall"
|
|
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
// Policy 必须由 root 独立维护,不能复用 Scert 可写的应用配置。
|
|
type Policy struct {
|
|
Version int `json:"version"`
|
|
AllowedUID *uint32 `json:"allowed_uid"`
|
|
SocketPath string `json:"socket_path"`
|
|
Directory string `json:"directory"`
|
|
Hostname string `json:"hostname"`
|
|
ProxyAddress string `json:"proxy_address"`
|
|
UpstreamAddress string `json:"upstream_address"`
|
|
NginxBinary string `json:"nginx_binary"`
|
|
NginxConfig string `json:"nginx_config"`
|
|
NginxPrefix string `json:"nginx_prefix"`
|
|
}
|
|
|
|
func LoadPolicy(path string) (Policy, error) {
|
|
return loadPolicy(path, 0)
|
|
}
|
|
|
|
func loadPolicy(path string, owner uint32) (Policy, error) {
|
|
var policy Policy
|
|
data, err := readTrustedPolicy(path, owner)
|
|
if err != nil {
|
|
return policy, err
|
|
}
|
|
decoder := json.NewDecoder(bytes.NewReader(data))
|
|
decoder.DisallowUnknownFields()
|
|
if err := decoder.Decode(&policy); err != nil {
|
|
return policy, err
|
|
}
|
|
var extra any
|
|
if err := decoder.Decode(&extra); err != io.EOF {
|
|
return policy, errors.New("激活策略包含多余内容")
|
|
}
|
|
if policy.Version != 1 || policy.AllowedUID == nil || *policy.AllowedUID == 0 {
|
|
return policy, errors.New("激活策略版本或非特权调用者 UID 无效")
|
|
}
|
|
if !filepath.IsAbs(policy.SocketPath) || filepath.Clean(policy.SocketPath) != policy.SocketPath || policy.SocketPath == "/" || len(policy.SocketPath) > 107 {
|
|
return policy, errors.New("激活 socket 路径无效")
|
|
}
|
|
if err := validateTrustedParents(filepath.Dir(policy.SocketPath), owner); err != nil {
|
|
return policy, err
|
|
}
|
|
if err := validateDirectory(policy.Directory); err != nil {
|
|
return policy, err
|
|
}
|
|
rotation := policy.Rotation()
|
|
if err := errors.Join(rotation.Proxy.validate(), rotation.Upstream.validate()); err != nil {
|
|
return policy, err
|
|
}
|
|
if policy.ProxyAddress == policy.UpstreamAddress {
|
|
return policy, errors.New("代理和上游不能使用同一地址")
|
|
}
|
|
if err := validateOwnedFile(policy.NginxBinary, owner, true); err != nil {
|
|
return policy, err
|
|
}
|
|
if err := validateOwnedFile(policy.NginxConfig, owner, false); err != nil {
|
|
return policy, err
|
|
}
|
|
if !filepath.IsAbs(policy.NginxPrefix) || filepath.Clean(policy.NginxPrefix) != policy.NginxPrefix {
|
|
return policy, errors.New("Nginx 前缀必须是规范绝对目录")
|
|
}
|
|
if err := validateTrustedParents(policy.NginxPrefix, owner); err != nil {
|
|
return policy, err
|
|
}
|
|
return policy, nil
|
|
}
|
|
|
|
func (p Policy) Rotation() Rotation {
|
|
return Rotation{
|
|
Directory: p.Directory,
|
|
Proxy: Endpoint{Address: p.ProxyAddress, Hostname: p.Hostname},
|
|
Upstream: Endpoint{Address: p.UpstreamAddress, Hostname: p.Hostname},
|
|
Reloader: CommandReloader{Binary: p.NginxBinary, Config: p.NginxConfig, Prefix: p.NginxPrefix},
|
|
}
|
|
}
|
|
|
|
func validateOwnedFile(path string, owner uint32, executable bool) error {
|
|
if !filepath.IsAbs(path) || filepath.Clean(path) != path {
|
|
return errors.New("激活策略文件必须使用规范绝对路径")
|
|
}
|
|
if err := validateTrustedParents(filepath.Dir(path), owner); err != nil {
|
|
return err
|
|
}
|
|
info, err := os.Lstat(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
stat, ok := info.Sys().(*syscall.Stat_t)
|
|
if !ok || !info.Mode().IsRegular() || stat.Uid != owner || stat.Nlink != 1 || info.Mode().Perm()&0o022 != 0 {
|
|
return errors.New("激活策略文件类型、所有者或写权限不可信")
|
|
}
|
|
if executable && (info.Mode().Perm()&0o100 == 0 || info.Mode()&(os.ModeSetuid|os.ModeSetgid) != 0) {
|
|
return errors.New("Nginx 可执行文件权限无效")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func readTrustedPolicy(path string, owner uint32) ([]byte, error) {
|
|
if err := validateOwnedFile(path, owner, false); err != nil {
|
|
return nil, err
|
|
}
|
|
file, err := os.OpenFile(path, os.O_RDONLY|unix.O_NOFOLLOW|unix.O_NONBLOCK, 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer file.Close()
|
|
info, err := file.Stat()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
stat, ok := info.Sys().(*syscall.Stat_t)
|
|
if !ok || !info.Mode().IsRegular() || stat.Uid != owner || stat.Nlink != 1 || info.Mode().Perm()&0o022 != 0 || info.Size() > 16<<10 {
|
|
return nil, errors.New("激活策略文件不可信或超过限制")
|
|
}
|
|
data, err := io.ReadAll(io.LimitReader(file, (16<<10)+1))
|
|
if err != nil || len(data) > 16<<10 {
|
|
return nil, errors.New("读取激活策略失败或超过限制")
|
|
}
|
|
return data, nil
|
|
}
|