chore: GORM remediation

This commit is contained in:
GitHub Actions
2026-01-28 18:47:52 +00:00
parent 243bce902a
commit 5bcf889f84
30 changed files with 641 additions and 57 deletions

3
.gitignore vendored
View File

@@ -301,3 +301,6 @@ docs/plans/supply_chain_security_implementation.md.backup
/playwright/.cache/
/playwright/.auth/
test-data/**
# GORM Security Scanner Reports
docs/reports/gorm-scan-*.txt

View File

@@ -34,10 +34,8 @@ func (h *DNSProviderHandler) List(c *gin.Context) {
// Convert to response format with has_credentials indicator
responses := make([]services.DNSProviderResponse, len(providers))
for i, p := range providers {
responses[i] = services.DNSProviderResponse{
DNSProvider: p,
HasCredentials: p.CredentialsEncrypted != "",
}
pCopy := p // Create a copy to take address of
responses[i] = services.NewDNSProviderResponse(&pCopy)
}
c.JSON(http.StatusOK, gin.H{
@@ -65,10 +63,7 @@ func (h *DNSProviderHandler) Get(c *gin.Context) {
return
}
response := services.DNSProviderResponse{
DNSProvider: *provider,
HasCredentials: provider.CredentialsEncrypted != "",
}
response := services.NewDNSProviderResponse(provider)
c.JSON(http.StatusOK, response)
}
@@ -101,10 +96,7 @@ func (h *DNSProviderHandler) Create(c *gin.Context) {
return
}
response := services.DNSProviderResponse{
DNSProvider: *provider,
HasCredentials: provider.CredentialsEncrypted != "",
}
response := services.NewDNSProviderResponse(provider)
c.JSON(http.StatusCreated, response)
}
@@ -144,10 +136,7 @@ func (h *DNSProviderHandler) Update(c *gin.Context) {
return
}
response := services.DNSProviderResponse{
DNSProvider: *provider,
HasCredentials: provider.CredentialsEncrypted != "",
}
response := services.NewDNSProviderResponse(provider)
c.JSON(http.StatusOK, response)
}

View File

@@ -209,7 +209,7 @@ func TestDNSProviderHandler_Get(t *testing.T) {
err := json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(t, err)
assert.Equal(t, uint(1), response.ID)
assert.Equal(t, "uuid-1", response.UUID)
assert.Equal(t, "Test Provider", response.Name)
assert.True(t, response.HasCredentials)
@@ -282,7 +282,7 @@ func TestDNSProviderHandler_Create(t *testing.T) {
err := json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(t, err)
assert.Equal(t, uint(1), response.ID)
assert.Equal(t, "uuid-1", response.UUID)
assert.Equal(t, "Test Provider", response.Name)
assert.True(t, response.HasCredentials)

View File

@@ -6,6 +6,7 @@ import (
"net"
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
@@ -27,9 +28,82 @@ type ProxyHostWarning struct {
}
// ProxyHostResponse wraps a proxy host with optional advisory warnings.
// Uses explicit fields to avoid exposing internal database IDs.
type ProxyHostResponse struct {
models.ProxyHost
Warnings []ProxyHostWarning `json:"warnings,omitempty"`
UUID string `json:"uuid"`
Name string `json:"name"`
DomainNames string `json:"domain_names"`
ForwardScheme string `json:"forward_scheme"`
ForwardHost string `json:"forward_host"`
ForwardPort int `json:"forward_port"`
SSLForced bool `json:"ssl_forced"`
HTTP2Support bool `json:"http2_support"`
HSTSEnabled bool `json:"hsts_enabled"`
HSTSSubdomains bool `json:"hsts_subdomains"`
BlockExploits bool `json:"block_exploits"`
WebsocketSupport bool `json:"websocket_support"`
Application string `json:"application"`
Enabled bool `json:"enabled"`
CertificateID *uint `json:"certificate_id"`
Certificate *models.SSLCertificate `json:"certificate,omitempty"`
AccessListID *uint `json:"access_list_id"`
AccessList *models.AccessList `json:"access_list,omitempty"`
Locations []models.Location `json:"locations"`
AdvancedConfig string `json:"advanced_config"`
AdvancedConfigBackup string `json:"advanced_config_backup"`
ForwardAuthEnabled bool `json:"forward_auth_enabled"`
WAFDisabled bool `json:"waf_disabled"`
SecurityHeaderProfileID *uint `json:"security_header_profile_id"`
SecurityHeaderProfile *models.SecurityHeaderProfile `json:"security_header_profile,omitempty"`
SecurityHeadersEnabled bool `json:"security_headers_enabled"`
SecurityHeadersCustom string `json:"security_headers_custom"`
EnableStandardHeaders *bool `json:"enable_standard_headers,omitempty"`
DNSProviderID *uint `json:"dns_provider_id,omitempty"`
DNSProvider *models.DNSProvider `json:"dns_provider,omitempty"`
UseDNSChallenge bool `json:"use_dns_challenge"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Warnings []ProxyHostWarning `json:"warnings,omitempty"`
}
// NewProxyHostResponse creates a ProxyHostResponse from a ProxyHost model.
func NewProxyHostResponse(host *models.ProxyHost, warnings []ProxyHostWarning) ProxyHostResponse {
return ProxyHostResponse{
UUID: host.UUID,
Name: host.Name,
DomainNames: host.DomainNames,
ForwardScheme: host.ForwardScheme,
ForwardHost: host.ForwardHost,
ForwardPort: host.ForwardPort,
SSLForced: host.SSLForced,
HTTP2Support: host.HTTP2Support,
HSTSEnabled: host.HSTSEnabled,
HSTSSubdomains: host.HSTSSubdomains,
BlockExploits: host.BlockExploits,
WebsocketSupport: host.WebsocketSupport,
Application: host.Application,
Enabled: host.Enabled,
CertificateID: host.CertificateID,
Certificate: host.Certificate,
AccessListID: host.AccessListID,
AccessList: host.AccessList,
Locations: host.Locations,
AdvancedConfig: host.AdvancedConfig,
AdvancedConfigBackup: host.AdvancedConfigBackup,
ForwardAuthEnabled: host.ForwardAuthEnabled,
WAFDisabled: host.WAFDisabled,
SecurityHeaderProfileID: host.SecurityHeaderProfileID,
SecurityHeaderProfile: host.SecurityHeaderProfile,
SecurityHeadersEnabled: host.SecurityHeadersEnabled,
SecurityHeadersCustom: host.SecurityHeadersCustom,
EnableStandardHeaders: host.EnableStandardHeaders,
DNSProviderID: host.DNSProviderID,
DNSProvider: host.DNSProvider,
UseDNSChallenge: host.UseDNSChallenge,
CreatedAt: host.CreatedAt,
UpdatedAt: host.UpdatedAt,
Warnings: warnings,
}
}
// generateForwardHostWarnings checks the forward_host value and returns advisory warnings.
@@ -176,10 +250,7 @@ func (h *ProxyHostHandler) Create(c *gin.Context) {
// Return response with warnings if any
if len(warnings) > 0 {
c.JSON(http.StatusCreated, ProxyHostResponse{
ProxyHost: host,
Warnings: warnings,
})
c.JSON(http.StatusCreated, NewProxyHostResponse(&host, warnings))
return
}
@@ -448,10 +519,7 @@ func (h *ProxyHostHandler) Update(c *gin.Context) {
// Return response with warnings if any
if len(warnings) > 0 {
c.JSON(http.StatusOK, ProxyHostResponse{
ProxyHost: *host,
Warnings: warnings,
})
c.JSON(http.StatusOK, NewProxyHostResponse(host, warnings))
return
}

View File

@@ -7,7 +7,7 @@ import (
// AccessList defines IP-based or auth-based access control rules
// that can be applied to proxy hosts.
type AccessList struct {
ID uint `json:"id" gorm:"primaryKey"`
ID uint `json:"-" gorm:"primaryKey"`
UUID string `json:"uuid" gorm:"uniqueIndex"`
Name string `json:"name" gorm:"index"`
Description string `json:"description"`

View File

@@ -6,8 +6,8 @@ import (
// CaddyConfig stores an audit trail of Caddy configuration changes.
type CaddyConfig struct {
ID uint `json:"id" gorm:"primaryKey"`
ConfigHash string `json:"config_hash" gorm:"index"`
ID uint `json:"-" gorm:"primaryKey"`
ConfigHash string `json:"-" gorm:"index"`
AppliedAt time.Time `json:"applied_at"`
Success bool `json:"success"`
ErrorMsg string `json:"error_msg"`

View File

@@ -4,7 +4,7 @@ import "time"
// CrowdsecConsoleEnrollment stores enrollment status and secrets for console registration.
type CrowdsecConsoleEnrollment struct {
ID uint `json:"id" gorm:"primarykey"`
ID uint `json:"-" gorm:"primaryKey"`
UUID string `json:"uuid" gorm:"uniqueIndex"`
Status string `json:"status" gorm:"index"`
Tenant string `json:"tenant"`

View File

@@ -4,7 +4,7 @@ import "time"
// CrowdsecPresetEvent captures audit trail for preset pull/apply events.
type CrowdsecPresetEvent struct {
ID uint `gorm:"primarykey" json:"id"`
ID uint `gorm:"primarykey" json:"-"`
Slug string `json:"slug"`
Action string `json:"action"`
Status string `json:"status"`

View File

@@ -8,7 +8,7 @@ import (
// DNSProvider represents a DNS provider configuration for ACME DNS-01 challenges.
// Credentials are stored encrypted at rest using AES-256-GCM.
type DNSProvider struct {
ID uint `json:"id" gorm:"primaryKey"`
ID uint `json:"-" gorm:"primaryKey"`
UUID string `json:"uuid" gorm:"uniqueIndex;size:36"`
Name string `json:"name" gorm:"index;not null;size:255"`
ProviderType string `json:"provider_type" gorm:"index;not null;size:50"`

View File

@@ -8,7 +8,7 @@ import (
// DNSProviderCredential represents a zone-specific credential set for a DNS provider.
// This allows different credentials to be used for different domains/zones within the same provider.
type DNSProviderCredential struct {
ID uint `json:"id" gorm:"primaryKey"`
ID uint `json:"-" gorm:"primaryKey"`
UUID string `json:"uuid" gorm:"uniqueIndex;size:36"`
DNSProviderID uint `json:"dns_provider_id" gorm:"index;not null"`
DNSProvider *DNSProvider `json:"dns_provider,omitempty" gorm:"foreignKey:DNSProviderID"`

View File

@@ -8,7 +8,7 @@ import (
)
type Domain struct {
ID uint `json:"id" gorm:"primarykey"`
ID uint `json:"-" gorm:"primaryKey"`
UUID string `json:"uuid" gorm:"uniqueIndex;not null"`
Name string `json:"name" gorm:"uniqueIndex;not null"`
CreatedAt time.Time `json:"created_at"`

View File

@@ -7,7 +7,7 @@ import (
// EmergencyToken stores metadata for database-backed emergency access tokens.
// Tokens are stored as bcrypt hashes for security.
type EmergencyToken struct {
ID uint `json:"id" gorm:"primaryKey"`
ID uint `json:"-" gorm:"primaryKey"`
TokenHash string `json:"-" gorm:"type:text;not null"` // bcrypt hash, never exposed in JSON
CreatedAt time.Time `json:"created_at"`
ExpiresAt *time.Time `json:"expires_at"` // NULL = never expires

View File

@@ -7,7 +7,7 @@ import (
// ImportSession tracks Caddyfile import operations with pending state
// until user reviews and confirms via UI.
type ImportSession struct {
ID uint `json:"id" gorm:"primaryKey"`
ID uint `json:"-" gorm:"primaryKey"`
UUID string `json:"uuid" gorm:"uniqueIndex"`
SourceFile string `json:"source_file"` // Path to original Caddyfile
Status string `json:"status" gorm:"default:'pending'"` // "pending", "reviewing", "committed", "rejected", "failed"

View File

@@ -6,7 +6,7 @@ import (
// Location represents a custom path-based proxy configuration within a ProxyHost.
type Location struct {
ID uint `json:"id" gorm:"primaryKey"`
ID uint `json:"-" gorm:"primaryKey"`
UUID string `json:"uuid" gorm:"uniqueIndex;not null"`
ProxyHostID uint `json:"proxy_host_id" gorm:"not null;index"`
Path string `json:"path" gorm:"not null;index"` // e.g., /api, /admin

View File

@@ -39,8 +39,8 @@ type ManualChallenge struct {
// Example: "_acme-challenge.example.com"
FQDN string `json:"fqdn" gorm:"index;not null;size:255"`
// Token is the ACME challenge token (for identification).
Token string `json:"token" gorm:"size:255"`
// Token is the ACME challenge token (for identification), never exposed in JSON.
Token string `json:"-" gorm:"size:255"`
// Value is the TXT record value that must be created.
Value string `json:"value" gorm:"not null;size:255"`

View File

@@ -5,7 +5,7 @@ import "time"
// Plugin represents an installed DNS provider plugin.
// This tracks both external .so plugins and their load status.
type Plugin struct {
ID uint `json:"id" gorm:"primaryKey"`
ID uint `json:"-" gorm:"primaryKey"`
UUID string `json:"uuid" gorm:"uniqueIndex;size:36"`
Name string `json:"name" gorm:"not null;size:255"`
Type string `json:"type" gorm:"uniqueIndex;not null;size:100"`

View File

@@ -6,7 +6,7 @@ import (
// ProxyHost represents a reverse proxy configuration.
type ProxyHost struct {
ID uint `json:"id" gorm:"primaryKey"`
ID uint `json:"-" gorm:"primaryKey"`
UUID string `json:"uuid" gorm:"uniqueIndex;not null"`
Name string `json:"name" gorm:"index"`
DomainNames string `json:"domain_names" gorm:"not null;index"` // Comma-separated list

View File

@@ -7,7 +7,7 @@ import (
// RemoteServer represents a known backend server that can be selected
// when creating proxy hosts, eliminating manual IP/port entry.
type RemoteServer struct {
ID uint `json:"id" gorm:"primaryKey"`
ID uint `json:"-" gorm:"primaryKey"`
UUID string `json:"uuid" gorm:"uniqueIndex"`
Name string `json:"name" gorm:"index"`
Provider string `json:"provider" gorm:"index"` // e.g., "docker", "vm", "cloud", "manual"

View File

@@ -6,7 +6,7 @@ import (
// SecurityAudit records admin actions or important changes related to security.
type SecurityAudit struct {
ID uint `json:"id" gorm:"primaryKey"`
ID uint `json:"-" gorm:"primaryKey"`
UUID string `json:"uuid" gorm:"uniqueIndex"`
Actor string `json:"actor" gorm:"index"`
Action string `json:"action"`

View File

@@ -7,7 +7,7 @@ import (
// SecurityConfig represents global Cerberus/CrowdSec/WAF/RateLimit settings
// used by the server and propagated into the generated Caddy config.
type SecurityConfig struct {
ID uint `json:"id" gorm:"primaryKey"`
ID uint `json:"-" gorm:"primaryKey"`
UUID string `json:"uuid" gorm:"uniqueIndex"`
Name string `json:"name" gorm:"index"`
Enabled bool `json:"enabled" gorm:"index"`

View File

@@ -7,7 +7,7 @@ import (
// SecurityDecision stores a decision/action taken by CrowdSec/WAF/RateLimit or manual
// override so it can be audited and surfaced in the UI.
type SecurityDecision struct {
ID uint `json:"id" gorm:"primaryKey"`
ID uint `json:"-" gorm:"primaryKey"`
UUID string `json:"uuid" gorm:"uniqueIndex"`
Source string `json:"source" gorm:"index"` // e.g., crowdsec, waf, ratelimit, manual
Action string `json:"action" gorm:"index"` // allow, block, challenge

View File

@@ -7,7 +7,7 @@ import (
// SecurityHeaderProfile stores reusable security header configurations.
// Users can create profiles and assign them to proxy hosts.
type SecurityHeaderProfile struct {
ID uint `json:"id" gorm:"primaryKey"`
ID uint `json:"-" gorm:"primaryKey"`
UUID string `json:"uuid" gorm:"uniqueIndex;not null"`
Name string `json:"name" gorm:"index;not null"`

View File

@@ -6,7 +6,7 @@ import (
// SecurityRuleSet stores metadata about WAF/CrowdSec rule sets that the server can download and apply.
type SecurityRuleSet struct {
ID uint `json:"id" gorm:"primaryKey"`
ID uint `json:"-" gorm:"primaryKey"`
UUID string `json:"uuid" gorm:"uniqueIndex"`
Name string `json:"name" gorm:"index"`
SourceURL string `json:"source_url" gorm:"type:text"`

View File

@@ -7,7 +7,7 @@ import (
// Setting stores global application configuration as key-value pairs.
// Used for system-wide preferences, feature flags, and runtime config.
type Setting struct {
ID uint `json:"id" gorm:"primaryKey"`
ID uint `json:"-" gorm:"primaryKey"`
Key string `json:"key" gorm:"uniqueIndex"`
Value string `json:"value" gorm:"type:text"`
Type string `json:"type" gorm:"index"` // "string", "int", "bool", "json"

View File

@@ -7,7 +7,7 @@ import (
// SSLCertificate represents TLS certificates managed by Charon.
// Can be Let's Encrypt auto-generated or custom uploaded certs.
type SSLCertificate struct {
ID uint `json:"id" gorm:"primaryKey"`
ID uint `json:"-" gorm:"primaryKey"`
UUID string `json:"uuid" gorm:"uniqueIndex"`
Name string `json:"name" gorm:"index"`
Provider string `json:"provider" gorm:"index"` // "letsencrypt", "custom", "self-signed"

View File

@@ -36,7 +36,7 @@ type UptimeMonitor struct {
}
type UptimeHeartbeat struct {
ID uint `gorm:"primaryKey" json:"id"`
ID uint `gorm:"primaryKey" json:"-"`
MonitorID string `json:"monitor_id" gorm:"index"`
Status string `json:"status"` // up, down
Latency int64 `json:"latency"`

View File

@@ -20,10 +20,10 @@ const (
// User represents authenticated users with role-based access control.
// Supports local auth, SSO integration, and invite-based onboarding.
type User struct {
ID uint `json:"id" gorm:"primaryKey"`
ID uint `json:"-" gorm:"primaryKey"`
UUID string `json:"uuid" gorm:"uniqueIndex"`
Email string `json:"email" gorm:"uniqueIndex"`
APIKey string `json:"api_key" gorm:"uniqueIndex"` // For external API access
APIKey string `json:"-" gorm:"uniqueIndex"` // For external API access, never exposed in JSON
PasswordHash string `json:"-"` // Never serialize password hash
Name string `json:"name"`
Role string `json:"role" gorm:"default:'user'"` // "admin", "user", "viewer"

View File

@@ -52,9 +52,46 @@ type UpdateDNSProviderRequest struct {
}
// DNSProviderResponse represents the API response for a DNS provider.
// Uses explicit fields to avoid exposing internal database IDs.
type DNSProviderResponse struct {
models.DNSProvider
HasCredentials bool `json:"has_credentials"`
UUID string `json:"uuid"`
Name string `json:"name"`
ProviderType string `json:"provider_type"`
Enabled bool `json:"enabled"`
IsDefault bool `json:"is_default"`
UseMultiCredentials bool `json:"use_multi_credentials"`
KeyVersion int `json:"key_version"`
PropagationTimeout int `json:"propagation_timeout"`
PollingInterval int `json:"polling_interval"`
LastUsedAt *time.Time `json:"last_used_at,omitempty"`
SuccessCount int `json:"success_count"`
FailureCount int `json:"failure_count"`
LastError string `json:"last_error,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
HasCredentials bool `json:"has_credentials"`
}
// NewDNSProviderResponse creates a DNSProviderResponse from a DNSProvider model.
func NewDNSProviderResponse(provider *models.DNSProvider) DNSProviderResponse {
return DNSProviderResponse{
UUID: provider.UUID,
Name: provider.Name,
ProviderType: provider.ProviderType,
Enabled: provider.Enabled,
IsDefault: provider.IsDefault,
UseMultiCredentials: provider.UseMultiCredentials,
KeyVersion: provider.KeyVersion,
PropagationTimeout: provider.PropagationTimeout,
PollingInterval: provider.PollingInterval,
LastUsedAt: provider.LastUsedAt,
SuccessCount: provider.SuccessCount,
FailureCount: provider.FailureCount,
LastError: provider.LastError,
CreatedAt: provider.CreatedAt,
UpdatedAt: provider.UpdatedAt,
HasCredentials: provider.CredentialsEncrypted != "",
}
}
// TestResult represents the result of testing DNS provider credentials.

View File

@@ -0,0 +1,486 @@
# GORM Security Issues Remediation Plan
**Status:** 🟡 **READY TO START**
**Created:** 2026-01-28
**Scanner Report:** 60 issues detected (28 CRITICAL, 2 HIGH, 33 MEDIUM)
**Estimated Total Time:** 8-12 hours
**Target Completion:** 2026-01-29
---
## Executive Summary
The GORM Security Scanner detected **60 pre-existing security issues** in the codebase. This plan provides a systematic approach to fix all issues, organized by priority and type.
**Issue Breakdown:**
- 🔴 **28 CRITICAL**: 22 ID leaks + 3 exposed secrets + 2 DTO issues + 1 emergency field
- 🔵 **33 MEDIUM**: Missing primary key tags (informational)
**Approach:**
1. Fix all CRITICAL issues (models, secrets, DTOs)
2. Optionally address MEDIUM issues (missing tags)
3. Verify with scanner
4. Run full test suite
5. Update CI to blocking mode
---
## Phase 1: Fix ID Leaks in Models (6-8 hours)
### Priority: 🔴 CRITICAL
### Estimated Time: 6-8 hours (22 models × 15-20 min each)
**Pattern:**
```go
// BEFORE (Vulnerable):
ID uint `json:"id" gorm:"primaryKey"`
// AFTER (Secure):
ID uint `json:"-" gorm:"primaryKey"`
```
### Task Checklist
#### Core Models (6 models)
- [ ] **User** (`internal/models/user.go:23`)
- [ ] **ProxyHost** (`internal/models/proxy_host.go:9`)
- [ ] **Domain** (`internal/models/domain.go:11`)
- [ ] **DNSProvider** (`internal/models/dns_provider.go:11`)
- [ ] **SSLCertificate** (`internal/models/ssl_certificate.go:10`)
- [ ] **AccessList** (`internal/models/access_list.go:10`)
#### Security Models (5 models)
- [ ] **SecurityConfig** (`internal/models/security_config.go:10`)
- [ ] **SecurityAudit** (`internal/models/security_audit.go:9`)
- [ ] **SecurityDecision** (`internal/models/security_decision.go:10`)
- [ ] **SecurityHeaderProfile** (`internal/models/security_header_profile.go:10`)
- [ ] **SecurityRuleset** (`internal/models/security_ruleset.go:9`)
#### Infrastructure Models (5 models)
- [ ] **Location** (`internal/models/location.go:9`)
- [ ] **Plugin** (`internal/models/plugin.go:8`)
- [ ] **RemoteServer** (`internal/models/remote_server.go:10`)
- [ ] **ImportSession** (`internal/models/import_session.go:10`)
- [ ] **Setting** (`internal/models/setting.go:10`)
#### Integration Models (3 models)
- [ ] **CrowdsecConsoleEnrollment** (`internal/models/crowdsec_console_enrollment.go:7`)
- [ ] **CrowdsecPresetEvent** (`internal/models/crowdsec_preset_event.go:7`)
- [ ] **CaddyConfig** (`internal/models/caddy_config.go:9`)
#### Provider & Monitoring Models (3 models)
- [ ] **DNSProviderCredential** (`internal/models/dns_provider_credential.go:11`)
- [ ] **EmergencyToken** (`internal/models/emergency_token.go:10`)
- [ ] **UptimeHeartbeat** (`internal/models/uptime.go:39`)
### Post-Model-Update Tasks
After changing each model:
1. **Update API handlers** that reference `.ID`:
```bash
# Find usages:
grep -r "\.ID" backend/internal/api/handlers/
grep -r "\"id\":" backend/internal/api/handlers/
```
2. **Update service layer** queries using `.ID`:
```bash
# Find usages:
grep -r "\.ID" backend/internal/services/
```
3. **Verify UUID field exists and is exposed**:
```go
UUID string `json:"uuid" gorm:"uniqueIndex"`
```
4. **Update tests** referencing `.ID`:
```bash
# Find test failures:
go test ./... -run TestModel
```
---
## Phase 2: Fix Exposed Secrets (30 minutes)
### Priority: 🔴 CRITICAL
### Estimated Time: 30 minutes (3 fields)
**Pattern:**
```go
// BEFORE (Vulnerable):
APIKey string `json:"api_key"`
// AFTER (Secure):
APIKey string `json:"-"`
```
### Task Checklist
- [ ] **User.APIKey** - Change to `json:"-"`
- Location: `internal/models/user.go`
- Verify: API key is never serialized to JSON
- [ ] **ManualChallenge.Token** - Change to `json:"-"`
- Location: `internal/services/manual_challenge_service.go:337`
- Verify: Challenge token is never exposed
- [ ] **CaddyConfig.ConfigHash** - Change to `json:"-"`
- Location: `internal/models/caddy_config.go`
- Verify: Config hash is never exposed
### Verification
```bash
# Ensure no secrets are exposed:
./scripts/scan-gorm-security.sh --report | grep "CRITICAL.*Secret"
# Should return: 0 results
```
---
## Phase 3: Fix Response DTO Embedding (1-2 hours)
### Priority: 🟡 HIGH
### Estimated Time: 1-2 hours (2 structs)
**Problem:** Response structs embed models, inheriting exposed IDs
**Pattern:**
```go
// BEFORE (Inherits ID exposure):
type ProxyHostResponse struct {
models.ProxyHost // Embeds entire model
Warnings []ProxyHostWarning `json:"warnings,omitempty"`
}
// AFTER (Explicit fields):
type ProxyHostResponse struct {
UUID string `json:"uuid"`
DomainNames []string `json:"domain_names"`
ForwardHost string `json:"forward_host"`
// ... other fields explicitly defined
Warnings []ProxyHostWarning `json:"warnings,omitempty"`
}
```
### Task Checklist
- [ ] **ProxyHostResponse** (`internal/api/handlers/proxy_host_handler.go:31`)
- [ ] Replace `models.ProxyHost` embedding with explicit fields
- [ ] Include `UUID string json:"uuid"` (expose external ID)
- [ ] Exclude `ID uint` (hide internal ID)
- [ ] Update all handler functions creating ProxyHostResponse
- [ ] Update tests
- [ ] **DNSProviderResponse** (`internal/services/dns_provider_service.go:56`)
- [ ] Replace `models.DNSProvider` embedding with explicit fields
- [ ] Include `UUID string json:"uuid"` (expose external ID)
- [ ] Exclude `ID uint` (hide internal ID)
- [ ] Keep `HasCredentials bool json:"has_credentials"`
- [ ] Update all service functions creating DNSProviderResponse
- [ ] Update tests
### Post-DTO-Update Tasks
1. **Update handler logic**:
```go
// Map model to response:
response := ProxyHostResponse{
UUID: model.UUID,
DomainNames: model.DomainNames,
// ... explicit field mapping
}
```
2. **Frontend coordination** (if needed):
- Frontend likely uses `uuid` already, not `id`
- Verify API client expectations
- Update TypeScript types if needed
---
## Phase 4: Fix Emergency Break Glass Field (15 minutes)
### Priority: 🔴 CRITICAL
### Estimated Time: 15 minutes (1 field)
**Issue:** `EmergencyConfig.BreakGlassEnabled` exposed
### Task Checklist
- [ ] **EmergencyConfig.BreakGlassEnabled**
- Location: Find in security models
- Change: Add `json:"-"` or verify it's informational only
- Verify: Emergency status not leaking sensitive info
---
## Phase 5: Optional - Fix Missing Primary Key Tags (1 hour)
### Priority: 🔵 MEDIUM (Informational)
### Estimated Time: 1 hour (33 fields)
### Decision: **OPTIONAL** - Can defer to separate issue
**Pattern:**
```go
// BEFORE (Missing tag):
ID uint `json:"-"`
// AFTER (Explicit tag):
ID uint `json:"-" gorm:"primaryKey"`
```
**Impact:** Missing tags don't cause security issues, but explicit tags improve:
- Query optimizer performance
- Code clarity
- GORM auto-migration accuracy
**Recommendation:** Create separate issue for this backlog item.
---
## Phase 6: Verification & Testing (1-2 hours)
### Task Checklist
#### 1. Scanner Verification (5 minutes)
- [ ] Run scanner in report mode:
```bash
./scripts/scan-gorm-security.sh --report
```
- [ ] Verify: **0 CRITICAL** and **0 HIGH** issues remain
- [ ] Optional: Verify **0 MEDIUM** if Phase 5 completed
#### 2. Backend Tests (30 minutes)
- [ ] Run full backend test suite:
```bash
cd backend && go test ./... -v
```
- [ ] Verify: All tests pass
- [ ] Fix any test failures related to ID → UUID changes
#### 3. Backend Coverage (15 minutes)
- [ ] Run coverage tests:
```bash
.github/skills/scripts/skill-runner.sh test-backend-coverage
```
- [ ] Verify: Coverage ≥85%
- [ ] Address any coverage drops
#### 4. Frontend Tests (if API changes) (30 minutes)
- [ ] TypeScript type check:
```bash
cd frontend && npm run type-check
```
- [ ] Run frontend tests:
```bash
.github/skills/scripts/skill-runner.sh test-frontend-coverage
```
- [ ] Verify: All tests pass
#### 5. Integration Tests (15 minutes)
- [ ] Start Docker environment:
```bash
docker-compose up -d
```
- [ ] Test affected endpoints:
- GET /api/proxy-hosts (verify UUID, no ID)
- GET /api/dns-providers (verify UUID, no ID)
- GET /api/users/me (verify no APIKey exposed)
#### 6. Final Scanner Check (5 minutes)
- [ ] Run scanner in check mode:
```bash
./scripts/scan-gorm-security.sh --check
echo "Exit code: $?" # Must be 0
```
- [ ] Verify: Exit code **0** (no issues)
---
## Phase 7: Enable Blocking in Pre-commit (5 minutes)
### Task Checklist
- [ ] Update `.pre-commit-config.yaml`:
```yaml
# CHANGE FROM:
stages: [manual]
# CHANGE TO:
stages: [commit]
```
- [ ] Test pre-commit hook:
```bash
pre-commit run --all-files
```
- [ ] Verify: Scanner runs on commit and passes
---
## Phase 8: Update Documentation (15 minutes)
### Task Checklist
- [ ] Update `docs/plans/gorm_security_remediation_plan.md`:
- Mark all tasks as complete
- Add "COMPLETED" status and completion date
- [ ] Update `docs/implementation/gorm_security_scanner_complete.md`:
- Update "Current Findings" section to show 0 issues
- Update pre-commit status to "Blocking"
- [ ] Update `docs/reports/gorm_scanner_qa_report.md`:
- Add remediation completion note
- [ ] Update `CHANGELOG.md`:
- Add entry: "Fixed 60 GORM security issues (ID leaks, exposed secrets)"
---
## Success Criteria
### Technical Success ✅
- [ ] Scanner reports **0 CRITICAL** and **0 HIGH** issues
- [ ] All backend tests pass
- [ ] Coverage maintained (≥85%)
- [ ] Pre-commit hook in blocking mode
- [ ] CI pipeline passes
### Security Success ✅
- [ ] No internal database IDs exposed via JSON
- [ ] No API keys/tokens/secrets exposed
- [ ] Response DTOs use explicit fields
- [ ] UUID fields used for all external references
### Documentation Success ✅
- [ ] Remediation plan marked complete
- [ ] Scanner documentation updated
- [ ] CHANGELOG updated
- [ ] Blocking mode documented
---
## Risk Mitigation
### Risk: Breaking API Changes
**Mitigation:**
- Frontend likely already uses `uuid` field, not `id`
- Test all API endpoints after changes
- Check frontend API client for `id` references
- Coordinate with frontend team if breaking changes needed
### Risk: Test Failures
**Mitigation:**
- Tests may hardcode `.ID` references
- Search and replace `.ID` → `.UUID` in tests
- Verify test fixtures use UUIDs
- Run tests incrementally after each model update
### Risk: Handler/Service Breakage
**Mitigation:**
- Use grep to find all `.ID` references before changing models
- Update handlers/services at the same time as models
- Test each endpoint after updating
- Use compiler errors as a guide (`.ID` will fail to serialize)
---
## Time Estimates Summary
| Phase | Priority | Time | Can Defer? |
|-------|----------|------|------------|
| Phase 1: ID Leaks (22 models) | 🔴 CRITICAL | 6-8 hours | ❌ No |
| Phase 2: Secrets (3 fields) | 🔴 CRITICAL | 30 min | ❌ No |
| Phase 3: DTO Embedding (2 structs) | 🟡 HIGH | 1-2 hours | ❌ No |
| Phase 4: Emergency Field (1 field) | 🔴 CRITICAL | 15 min | ❌ No |
| Phase 5: Missing Tags (33 fields) | 🔵 MEDIUM | 1 hour | ✅ Yes |
| Phase 6: Verification & Testing | Required | 1-2 hours | ❌ No |
| Phase 7: Enable Blocking | Required | 5 min | ❌ No |
| Phase 8: Documentation | Required | 15 min | ❌ No |
| **TOTAL (Required)** | | **9.5-13 hours** | |
| **TOTAL (with optional)** | | **10.5-14 hours** | |
---
## Quick Start Guide for Tomorrow
### Morning Session (4-5 hours)
1. **Start Scanner** to see baseline:
```bash
./scripts/scan-gorm-security.sh --report | tee gorm-before.txt
```
2. **Tackle Core Models** (User, ProxyHost, Domain, DNSProvider, SSLCertificate, AccessList):
- For each model:
- Change `json:"id"` → `json:"-"`
- Verify `UUID` field exists
- Run: `go test ./internal/models/`
- Fix compilation errors
- Batch commit: "fix: hide internal IDs in core GORM models"
3. **Coffee Break** ☕
### Afternoon Session (4-5 hours)
4. **Tackle Remaining Models** (Security, Infrastructure, Integration models):
- Same pattern as morning
- Batch commit: "fix: hide internal IDs in remaining GORM models"
5. **Fix Exposed Secrets** (Phase 2):
- Quick 30-minute task
- Commit: "fix: hide API keys and sensitive fields"
6. **Fix Response DTOs** (Phase 3):
- ProxyHostResponse
- DNSProviderResponse
- Commit: "refactor: use explicit fields in response DTOs"
7. **Final Verification** (Phase 6):
- Run scanner: `./scripts/scan-gorm-security.sh --check`
- Run tests: `go test ./...`
- Run coverage: `.github/skills/scripts/skill-runner.sh test-backend-coverage`
8. **Enable & Document** (Phases 7-8):
- Update pre-commit config to blocking mode
- Update all docs
- Final commit: "chore: enable GORM security scanner blocking mode"
---
## Post-Remediation
After all fixes are complete:
1. **Create PR** with all remediation commits
2. **CI will pass** (scanner finds 0 issues)
3. **Merge to main**
4. **Close chore item** in `docs/plans/chores.md`
5. **Celebrate** 🎉 - Charon is now more secure!
---
## Notes
- **Current Branch:** `feature/beta-release`
- **Scanner Location:** `scripts/scan-gorm-security.sh`
- **Documentation:** `docs/implementation/gorm_security_scanner_complete.md`
- **QA Report:** `docs/reports/gorm_scanner_qa_report.md`
**Tips:**
- Work in small batches (5-6 models at a time)
- Test incrementally (don't fix everything before testing)
- Use grep to find all `.ID` references before each change
- Commit frequently to make rollback easier if needed
- Take breaks - this is tedious but important work!
**Good luck tomorrow! 💪**

View File

@@ -331,7 +331,8 @@ detect_missing_primary_key() {
report_issue "MEDIUM" "MISSING-PK" "$file" "$line_num" "${struct_name:-Unknown}" \
"ID Field Missing Primary Key Tag" \
"💡 Fix: Add 'primaryKey' to gorm tag: gorm:\"primaryKey\""
done < <(grep -n 'ID.*gorm:' "$file" 2>/dev/null || true)
# Only match primary key ID field (not foreign keys like CertificateID, AccessListID, etc.)
done < <(grep -n -E '^\s+ID\s+' "$file" 2>/dev/null || true)
done < <(find "$SCAN_DIR/internal/models" -name "*.go" -type f 2>/dev/null || true)
}