Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
75d945f
fix: ensure ACL and Security Headers dropdown selections persist corr…
actions-user Feb 27, 2026
c9e4916
fix: update SelectContent styles to improve z-index and pointer event…
actions-user Feb 27, 2026
5c0185d
Merge branch 'development' into feature/beta-release
Wikid82 Feb 27, 2026
2024ad1
fix: enhance AccessListSelector and ProxyHostForm to support UUID-onl…
actions-user Feb 28, 2026
5c4a558
chore: enhance ACL handling in dropdowns and add emergency token flows
actions-user Feb 28, 2026
6ed8d80
fix: update getOptionToken to handle string IDs correctly
actions-user Feb 28, 2026
bf58392
fix: improve ID parsing logic in AccessListSelector and ProxyHostForm…
actions-user Feb 28, 2026
0ff19f6
fix: update resolveAccessListToken to handle accessLists and improve …
actions-user Feb 28, 2026
b04b94e
fix: enhance access list handling in ProxyHostHandler and forms to su…
actions-user Feb 28, 2026
cdf7948
fix: update access list handling in ProxyHostService and forms to sup…
actions-user Feb 28, 2026
5fe1cf9
fix: enhance security header profile handling in ProxyHost to support…
actions-user Feb 28, 2026
be279ba
fix: update oxc-resolver package versions to 11.19.1 in package-lock.…
actions-user Feb 28, 2026
25443d3
fix(deps): update module github.com/gin-gonic/gin to v1.12.0
renovate[bot] Feb 28, 2026
de7861a
Merge pull request #777 from Wikid82/renovate/feature/beta-release-no…
Wikid82 Feb 28, 2026
89281c4
fix: add UUID validation in resolveSecurityHeaderProfileReference method
actions-user Feb 28, 2026
10f5e5d
chore: enhance coverage for AccessListSelector and ProxyHostForm comp…
actions-user Feb 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Fixed: Added robust validation and debug logging for Docker image tags to prevent invalid reference errors.
- Fixed: Removed log masking for image references and added manifest validation to debug CI failures.
- **Proxy Hosts**: Fixed ACL and Security Headers dropdown selections so create/edit saves now keep the selected values (including clearing to none) after submit and reload.
- **CI**: Fixed Docker image reference output so integration jobs never pull an empty image ref
- **E2E Test Reliability**: Resolved test timeout issues affecting CI/CD pipeline stability
- Fixed config reload overlay blocking test interactions
Expand Down
11 changes: 6 additions & 5 deletions backend/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ go 1.26
require (
github.com/docker/docker v28.5.2+incompatible
github.com/gin-contrib/gzip v1.2.5
github.com/gin-gonic/gin v1.11.0
github.com/gin-gonic/gin v1.12.0
github.com/glebarez/sqlite v1.11.0
github.com/golang-jwt/jwt/v5 v5.3.1
github.com/google/uuid v1.6.0
Expand All @@ -29,8 +29,8 @@ require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.14.1 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/bytedance/sonic v1.15.0 // indirect
github.com/bytedance/sonic/loader v0.5.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
Expand All @@ -51,7 +51,7 @@ require (
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.30.1 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
Expand Down Expand Up @@ -80,7 +80,8 @@ require (
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A new transitive dependency go.mongodb.org/mongo-driver/v2 v2.5.0 appears in go.mod as an indirect dependency. MongoDB driver is unexpected for what appears to be a SQLite/GORM-based backend. This likely came in transitively via the gin v1.12.0 upgrade. The presence of a large, unneeded transitive dependency should be verified — if it's not actually needed, go mod tidy should be run to confirm its necessity or remove it. This significantly increases binary size and attack surface.

Suggested change
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect

Copilot uses AI. Check for mistakes.
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
go.opentelemetry.io/otel v1.38.0 // indirect
Expand Down
14 changes: 14 additions & 0 deletions backend/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@ github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w=
github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc=
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
Expand Down Expand Up @@ -45,6 +49,8 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
Expand All @@ -66,6 +72,8 @@ github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
Expand Down Expand Up @@ -162,12 +170,18 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
Expand Down
162 changes: 110 additions & 52 deletions backend/internal/api/handlers/proxy_host_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ func generateForwardHostWarnings(forwardHost string) []ProxyHostWarning {
// ProxyHostHandler handles CRUD operations for proxy hosts.
type ProxyHostHandler struct {
service *services.ProxyHostService
db *gorm.DB
caddyManager *caddy.Manager
notificationService *services.NotificationService
uptimeService *services.UptimeService
Expand Down Expand Up @@ -183,6 +184,74 @@ func parseNullableUintField(value any, fieldName string) (*uint, bool, error) {
}
}

func (h *ProxyHostHandler) resolveAccessListReference(value any) (*uint, error) {
if value == nil {
return nil, nil
}

parsedID, _, parseErr := parseNullableUintField(value, "access_list_id")
if parseErr == nil {
return parsedID, nil
}

uuidValue, isString := value.(string)
if !isString {
return nil, parseErr
}

trimmed := strings.TrimSpace(uuidValue)
if trimmed == "" {
return nil, nil
}

var acl models.AccessList
if err := h.db.Select("id").Where("uuid = ?", trimmed).First(&acl).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, fmt.Errorf("access list not found")
}
return nil, fmt.Errorf("failed to resolve access list")
}

id := acl.ID
return &id, nil
Comment on lines +202 to +216
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The resolveAccessListReference function does not validate UUID format before attempting the database query (unlike resolveSecurityHeaderProfileReference which validates with uuid.Parse). This means any arbitrary non-numeric string (e.g. a malformed UUID or an SQL injection attempt) will be passed directly to the WHERE clause. While GORM uses parameterized queries preventing SQL injection, the asymmetry between the two resolve functions is a design inconsistency. If the intent is to accept only valid UUIDs as string references, UUID validation should be added here as well.

Copilot uses AI. Check for mistakes.
}

func (h *ProxyHostHandler) resolveSecurityHeaderProfileReference(value any) (*uint, error) {
if value == nil {
return nil, nil
}

parsedID, _, parseErr := parseNullableUintField(value, "security_header_profile_id")
if parseErr == nil {
return parsedID, nil
}

uuidValue, isString := value.(string)
if !isString {
return nil, parseErr
}

trimmed := strings.TrimSpace(uuidValue)
if trimmed == "" {
return nil, nil
}

if _, err := uuid.Parse(trimmed); err != nil {
return nil, parseErr
}

var profile models.SecurityHeaderProfile
if err := h.db.Select("id").Where("uuid = ?", trimmed).First(&profile).Error; err != nil {
if err == gorm.ErrRecordNotFound {
Comment on lines +209 to +245
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new resolveAccessListReference and resolveSecurityHeaderProfileReference functions use err == gorm.ErrRecordNotFound for error comparison, but the established codebase convention is to use errors.Is(err, gorm.ErrRecordNotFound) (as seen consistently in security_handler.go and settings_handler.go). Using direct equality (==) instead of errors.Is can fail to match wrapped errors, which means the "not found" branch may never be taken and the function would return a generic "failed to resolve" error instead of the more specific "not found" message.

Copilot uses AI. Check for mistakes.
return nil, fmt.Errorf("security header profile not found")
}
return nil, fmt.Errorf("failed to resolve security header profile")
}

id := profile.ID
return &id, nil
}

func parseForwardPortField(value any) (int, error) {
switch v := value.(type) {
case float64:
Expand Down Expand Up @@ -221,6 +290,7 @@ func parseForwardPortField(value any) (int, error) {
func NewProxyHostHandler(db *gorm.DB, caddyManager *caddy.Manager, ns *services.NotificationService, uptimeService *services.UptimeService) *ProxyHostHandler {
return &ProxyHostHandler{
service: services.NewProxyHostService(db),
db: db,
caddyManager: caddyManager,
notificationService: ns,
uptimeService: uptimeService,
Expand Down Expand Up @@ -252,8 +322,38 @@ func (h *ProxyHostHandler) List(c *gin.Context) {

// Create creates a new proxy host.
func (h *ProxyHostHandler) Create(c *gin.Context) {
var payload map[string]any
if err := c.ShouldBindJSON(&payload); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

if rawAccessListRef, ok := payload["access_list_id"]; ok {
resolvedAccessListID, resolveErr := h.resolveAccessListReference(rawAccessListRef)
if resolveErr != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": resolveErr.Error()})
return
}
payload["access_list_id"] = resolvedAccessListID
}

if rawSecurityHeaderRef, ok := payload["security_header_profile_id"]; ok {
resolvedSecurityHeaderID, resolveErr := h.resolveSecurityHeaderProfileReference(rawSecurityHeaderRef)
if resolveErr != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": resolveErr.Error()})
return
}
payload["security_header_profile_id"] = resolvedSecurityHeaderID
}

payloadBytes, marshalErr := json.Marshal(payload)
if marshalErr != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request payload"})
return
}

var host models.ProxyHost
if err := c.ShouldBindJSON(&host); err != nil {
if err := json.Unmarshal(payloadBytes, &host); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
Expand Down Expand Up @@ -430,12 +530,12 @@ func (h *ProxyHostHandler) Update(c *gin.Context) {
host.CertificateID = parsedID
}
if v, ok := payload["access_list_id"]; ok {
parsedID, _, parseErr := parseNullableUintField(v, "access_list_id")
if parseErr != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": parseErr.Error()})
resolvedAccessListID, resolveErr := h.resolveAccessListReference(v)
if resolveErr != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": resolveErr.Error()})
return
}
host.AccessListID = parsedID
host.AccessListID = resolvedAccessListID
}

if v, ok := payload["dns_provider_id"]; ok {
Expand All @@ -453,54 +553,12 @@ func (h *ProxyHostHandler) Update(c *gin.Context) {

// Security Header Profile: update only if provided
if v, ok := payload["security_header_profile_id"]; ok {
logger := middleware.GetRequestLogger(c)
// Sanitize user-provided values for log injection protection (CWE-117)
safeUUID := sanitizeForLog(uuidStr)
logger.WithField("host_uuid", safeUUID).WithField("raw_value", sanitizeForLog(fmt.Sprintf("%v", v))).Debug("Processing security_header_profile_id update")

if v == nil {
logger.WithField("host_uuid", safeUUID).Debug("Setting security_header_profile_id to nil")
host.SecurityHeaderProfileID = nil
} else {
conversionSuccess := false
switch t := v.(type) {
case float64:
logger.Debug("Received security_header_profile_id as float64")
if id, ok := safeFloat64ToUint(t); ok {
host.SecurityHeaderProfileID = &id
conversionSuccess = true
logger.Info("Successfully converted security_header_profile_id from float64")
} else {
logger.Warn("Failed to convert security_header_profile_id from float64: value is negative or not a valid uint")
}
case int:
logger.Debug("Received security_header_profile_id as int")
if id, ok := safeIntToUint(t); ok {
host.SecurityHeaderProfileID = &id
conversionSuccess = true
logger.Info("Successfully converted security_header_profile_id from int")
} else {
logger.Warn("Failed to convert security_header_profile_id from int: value is negative")
}
case string:
logger.Debug("Received security_header_profile_id as string")
if n, err := strconv.ParseUint(t, 10, 32); err == nil {
id := uint(n)
host.SecurityHeaderProfileID = &id
conversionSuccess = true
logger.WithField("host_uuid", safeUUID).WithField("profile_id", id).Info("Successfully converted security_header_profile_id from string")
} else {
logger.Warn("Failed to parse security_header_profile_id from string")
}
default:
logger.Warn("Unsupported type for security_header_profile_id")
}

if !conversionSuccess {
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("invalid security_header_profile_id: unable to convert value %v of type %T to uint", v, v)})
return
}
resolvedSecurityHeaderID, resolveErr := h.resolveSecurityHeaderProfileReference(v)
if resolveErr != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": resolveErr.Error()})
return
}
host.SecurityHeaderProfileID = resolvedSecurityHeaderID
}

// Locations: replace only if provided
Expand Down
Loading
Loading