security fix

This commit is contained in:
2026-07-13 00:57:28 -03:00
parent ba5b581aaf
commit 9001b47204
24 changed files with 1706 additions and 677 deletions
+204 -30
View File
@@ -8,6 +8,7 @@ import (
"fmt"
"io"
"log"
"net"
"net/http"
"net/url"
"strconv"
@@ -15,6 +16,25 @@ import (
"time"
)
const managedCredentialPrefix = "enc:v1:"
func sealManagedCredential(plain string) (string, error) {
return sealCredential(managedCredentialPrefix, plain)
}
func openManagedCredential(stored string) (string, error) {
return openCredential(managedCredentialPrefix, stored)
}
func managedServerHTTPClient(timeout time.Duration) *http.Client {
return &http.Client{
Timeout: timeout,
CheckRedirect: func(_ *http.Request, _ []*http.Request) error {
return http.ErrUseLastResponse
},
}
}
type ManagedServer struct {
ID int
Name string
@@ -66,7 +86,43 @@ func (s *Store) EnsureManagedServersSchema(ctx context.Context) error {
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)`)
return err
if err != nil {
return err
}
return s.migrateManagedServerCredentials(ctx)
}
func (s *Store) migrateManagedServerCredentials(ctx context.Context) error {
rows, err := s.db.QueryContext(ctx, `SELECT id, admin_key FROM managed_servers WHERE admin_key <> '' AND admin_key NOT LIKE 'enc:v1:%'`)
if err != nil {
return err
}
type legacyCredential struct {
id int
key string
}
var legacy []legacyCredential
for rows.Next() {
var item legacyCredential
if err := rows.Scan(&item.id, &item.key); err != nil {
rows.Close()
return err
}
legacy = append(legacy, item)
}
if err := rows.Close(); err != nil {
return err
}
for _, item := range legacy {
sealed, err := sealManagedCredential(item.key)
if err != nil {
return fmt.Errorf("encrypt managed server credential %d: %w", item.id, err)
}
if _, err := s.db.ExecContext(ctx, `UPDATE managed_servers SET admin_key=$2 WHERE id=$1`, item.id, sealed); err != nil {
return err
}
}
return nil
}
func (s *Store) ListManagedServers(ctx context.Context) ([]*ManagedServer, error) {
@@ -83,6 +139,11 @@ func (s *Store) ListManagedServers(ctx context.Context) ([]*ManagedServer, error
if err := rows.Scan(&ms.ID, &ms.Name, &ms.BaseURL, &ms.AdminUsername, &ms.AdminKey, &ms.EnableSSH, &ms.EnableXray, &ms.IsActive, &ms.CreatedAt, &ms.UpdatedAt); err != nil {
return nil, err
}
plainKey, err := openManagedCredential(ms.AdminKey)
if err != nil {
return nil, err
}
ms.AdminKey = plainKey
out = append(out, ms)
}
return out, rows.Err()
@@ -100,12 +161,17 @@ func (s *Store) GetManagedServer(ctx context.Context, id int) (*ManagedServer, e
if err != nil {
return nil, err
}
plainKey, err := openManagedCredential(ms.AdminKey)
if err != nil {
return nil, err
}
ms.AdminKey = plainKey
return ms, nil
}
func (s *Store) UpsertManagedServer(ctx context.Context, p ManagedServerPayload) (*ManagedServer, error) {
name := strings.TrimSpace(p.Name)
baseURL := normalizeManagedServerBaseURL(p.BaseURL)
baseURL, baseURLErr := validateManagedServerBaseURL(p.BaseURL)
adminUsername := strings.TrimSpace(p.AdminUsername)
if adminUsername == "" {
adminUsername = "admin"
@@ -113,8 +179,17 @@ func (s *Store) UpsertManagedServer(ctx context.Context, p ManagedServerPayload)
if name == "" {
return nil, fmt.Errorf("server name required")
}
if baseURL == "" {
return nil, fmt.Errorf("base url required")
if len(name) > 120 || strings.IndexFunc(name, func(r rune) bool { return r < 0x20 || r == 0x7f }) >= 0 {
return nil, fmt.Errorf("invalid server name")
}
if len(adminUsername) > 128 || strings.IndexFunc(adminUsername, func(r rune) bool { return r < 0x20 || r == 0x7f }) >= 0 {
return nil, fmt.Errorf("invalid admin username")
}
if len(p.AdminKey) > 4096 || strings.IndexFunc(p.AdminKey, func(r rune) bool { return r < 0x20 || r == 0x7f }) >= 0 {
return nil, fmt.Errorf("invalid admin credential")
}
if baseURLErr != nil {
return nil, baseURLErr
}
if p.ID != "" && p.ID != "local" {
id, err := strconv.Atoi(p.ID)
@@ -127,10 +202,14 @@ func (s *Store) UpsertManagedServer(ctx context.Context, p ManagedServerPayload)
SET name=$2, base_url=$3, admin_username=$4, enable_ssh=$5, enable_xray=$6, is_active=$7, updated_at=NOW()
WHERE id=$1`, id, name, baseURL, adminUsername, p.EnableSSH, p.EnableXray, p.IsActive)
} else {
sealedKey, sealErr := sealManagedCredential(p.AdminKey)
if sealErr != nil {
return nil, fmt.Errorf("encrypt admin credential: %w", sealErr)
}
_, err = s.db.ExecContext(ctx, `
UPDATE managed_servers
SET name=$2, base_url=$3, admin_username=$4, admin_key=$5, enable_ssh=$6, enable_xray=$7, is_active=$8, updated_at=NOW()
WHERE id=$1`, id, name, baseURL, adminUsername, p.AdminKey, p.EnableSSH, p.EnableXray, p.IsActive)
WHERE id=$1`, id, name, baseURL, adminUsername, sealedKey, p.EnableSSH, p.EnableXray, p.IsActive)
}
if err != nil {
return nil, err
@@ -140,8 +219,12 @@ func (s *Store) UpsertManagedServer(ctx context.Context, p ManagedServerPayload)
if strings.TrimSpace(p.AdminKey) == "" {
return nil, fmt.Errorf("admin key/password required")
}
sealedKey, err := sealManagedCredential(p.AdminKey)
if err != nil {
return nil, fmt.Errorf("encrypt admin credential: %w", err)
}
var id int
err := s.db.QueryRowContext(ctx, `
err = s.db.QueryRowContext(ctx, `
INSERT INTO managed_servers (name, base_url, admin_username, admin_key, enable_ssh, enable_xray, is_active)
VALUES ($1,$2,$3,$4,$5,$6,$7)
ON CONFLICT (base_url) DO UPDATE SET
@@ -152,7 +235,7 @@ func (s *Store) UpsertManagedServer(ctx context.Context, p ManagedServerPayload)
enable_xray=EXCLUDED.enable_xray,
is_active=EXCLUDED.is_active,
updated_at=NOW()
RETURNING id`, name, baseURL, adminUsername, p.AdminKey, p.EnableSSH, p.EnableXray, p.IsActive).Scan(&id)
RETURNING id`, name, baseURL, adminUsername, sealedKey, p.EnableSSH, p.EnableXray, p.IsActive).Scan(&id)
if err != nil {
return nil, err
}
@@ -193,21 +276,39 @@ func localManagedServerDTO() ManagedServerDTO {
}
func normalizeManagedServerBaseURL(raw string) string {
normalized, _ := validateManagedServerBaseURL(raw)
return normalized
}
func validateManagedServerBaseURL(raw string) (string, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return ""
return "", fmt.Errorf("base url required")
}
if !strings.HasPrefix(raw, "http://") && !strings.HasPrefix(raw, "https://") {
lowerRaw := strings.ToLower(raw)
if !strings.HasPrefix(lowerRaw, "http://") && !strings.HasPrefix(lowerRaw, "https://") {
raw = "http://" + raw
}
u, err := url.Parse(raw)
if err != nil || u.Scheme == "" || u.Host == "" {
return ""
return "", fmt.Errorf("invalid base url")
}
u.Path = strings.TrimRight(u.Path, "/")
if u.Scheme != "http" && u.Scheme != "https" {
return "", fmt.Errorf("base url must use http or https")
}
if u.User != nil {
return "", fmt.Errorf("base url must not contain credentials")
}
if u.Path != "" && u.Path != "/" {
return "", fmt.Errorf("base url must not contain a path")
}
if ip := net.ParseIP(u.Hostname()); ip != nil && (ip.IsUnspecified() || ip.IsMulticast() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast()) {
return "", fmt.Errorf("base url uses a forbidden address")
}
u.Path = ""
u.RawQuery = ""
u.Fragment = ""
return strings.TrimRight(u.String(), "/")
return strings.TrimRight(u.String(), "/"), nil
}
func requestedServerID(r *http.Request) string {
@@ -250,7 +351,7 @@ func remoteLoginToken(ctx context.Context, ms *ManagedServer) (string, error) {
return "", err
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 15 * time.Second}
client := managedServerHTTPClient(15 * time.Second)
resp, err := client.Do(req)
if err != nil {
return "", err
@@ -286,7 +387,7 @@ func proxyManagedServer(ctx context.Context, ms *ManagedServer, method, path str
}
req.Header.Set("Content-Type", contentType)
req.Header.Set("X-Session-Token", token)
client := &http.Client{Timeout: 30 * time.Second}
client := managedServerHTTPClient(30 * time.Second)
resp, err := client.Do(req)
if err != nil {
return 0, nil, "", err
@@ -560,46 +661,119 @@ func handleManagedServerConfig(store *Store) http.HandlerFunc {
}
}
func remoteSSHUserOwned(ctx context.Context, ms *ManagedServer, username, owner string) bool {
if owner == "" || username == "" {
return false
func remoteSSHUserOwner(ctx context.Context, ms *ManagedServer, username string) (owner string, exists bool, err error) {
if username == "" {
return "", false, nil
}
status, data, _, err := proxyManagedServer(ctx, ms, http.MethodGet, "/api/users", nil, "application/json")
if err != nil || status < 200 || status >= 300 {
return false
if err == nil {
err = fmt.Errorf("remote users returned HTTP %d", status)
}
return "", false, err
}
var rows []map[string]interface{}
if err := json.Unmarshal(data, &rows); err != nil {
return false
return "", false, err
}
for _, row := range rows {
if fmt.Sprint(row["username"]) == username && fmt.Sprint(row["owner_username"]) == owner {
return true
if fmt.Sprint(row["username"]) == username {
return strings.TrimSpace(fmt.Sprint(row["owner_username"])), true, nil
}
}
return false
return "", false, nil
}
func remoteXrayClientOwned(ctx context.Context, ms *ManagedServer, uuid, owner string) bool {
if owner == "" || uuid == "" {
return false
func remoteSSHUserOwned(ctx context.Context, ms *ManagedServer, username, owner string) bool {
actualOwner, exists, err := remoteSSHUserOwner(ctx, ms, username)
return err == nil && exists && actualOwner == owner
}
func remoteXrayClientOwner(ctx context.Context, ms *ManagedServer, uuid string) (owner string, exists bool, err error) {
if uuid == "" {
return "", false, nil
}
status, data, _, err := proxyManagedServer(ctx, ms, http.MethodGet, "/api/xray/inbounds", nil, "application/json")
if err != nil || status < 200 || status >= 300 {
return false
if err == nil {
err = fmt.Errorf("remote Xray inbounds returned HTTP %d", status)
}
return "", false, err
}
var inbounds []map[string]interface{}
if err := json.Unmarshal(data, &inbounds); err != nil {
return false
return "", false, err
}
for _, ib := range inbounds {
clients, _ := ib["clients"].([]interface{})
for _, c := range clients {
m, _ := c.(map[string]interface{})
if fmt.Sprint(m["id"]) == uuid && fmt.Sprint(m["owner_username"]) == owner {
return true
if fmt.Sprint(m["id"]) == uuid {
return strings.TrimSpace(fmt.Sprint(m["owner_username"])), true, nil
}
}
}
return false
return "", false, nil
}
func remoteXrayClientOwned(ctx context.Context, ms *ManagedServer, uuid, owner string) bool {
actualOwner, exists, err := remoteXrayClientOwner(ctx, ms, uuid)
return err == nil && exists && actualOwner == owner
}
func countOwnedQuotaAcrossManagedServers(ctx context.Context, store *Store, owner string) (int, error) {
if store == nil || owner == "" {
return 0, nil
}
total := countOwnedQuota(ctx, store, owner)
servers, err := store.ListManagedServers(ctx)
if err != nil {
return 0, err
}
for _, ms := range servers {
if !ms.IsActive {
continue
}
if ms.EnableSSH {
status, data, _, err := proxyManagedServer(ctx, ms, http.MethodGet, "/api/users", nil, "application/json")
if err != nil || status < 200 || status >= 300 {
if err == nil {
err = fmt.Errorf("remote users returned HTTP %d", status)
}
return 0, err
}
var users []map[string]interface{}
if err := json.Unmarshal(data, &users); err != nil {
return 0, err
}
for _, user := range users {
if strings.TrimSpace(fmt.Sprint(user["owner_username"])) == owner {
total++
}
}
}
if ms.EnableXray {
status, data, _, err := proxyManagedServer(ctx, ms, http.MethodGet, "/api/xray/inbounds", nil, "application/json")
if err != nil || status < 200 || status >= 300 {
if err == nil {
err = fmt.Errorf("remote Xray inbounds returned HTTP %d", status)
}
return 0, err
}
var inbounds []map[string]interface{}
if err := json.Unmarshal(data, &inbounds); err != nil {
return 0, err
}
for _, inbound := range inbounds {
clients, _ := inbound["clients"].([]interface{})
for _, client := range clients {
item, _ := client.(map[string]interface{})
if strings.TrimSpace(fmt.Sprint(item["owner_username"])) == owner {
total++
}
}
}
}
}
return total, nil
}