Skip to content

Latest commit

 

History

History
1251 lines (916 loc) · 36.2 KB

File metadata and controls

1251 lines (916 loc) · 36.2 KB

Security Assessment Report

GopherPass (LDAP Selfservice Password Changer)

Assessment Date: 2025-10-09 Assessed Version: main branch (commit 9592c1d) Assessment Scope: Comprehensive security analysis Methodology: OWASP Top 10 2021, CVSS v3.1 scoring, code review


Executive Summary

This comprehensive security assessment evaluated GopherPass across authentication, authorization, cryptography, input validation, API security, container security, and dependency management. The application demonstrates strong foundational security with excellent cryptographic practices, secure container configuration, and thoughtful stateless design. However, 5 high-severity and critical findings require immediate remediation to prevent exploitation.

Overall Security Posture: MODERATE

Key Strengths:

  • Cryptographically secure token generation (crypto/rand, 256-bit)
  • Excellent Docker security (scratch base, non-root execution, pinned images)
  • Stateless authentication design (no session vulnerabilities)
  • User enumeration prevention in password reset flow
  • Proper structured logging without credential leakage

Critical Gaps:

  • Case sensitivity bug in password validation (username inclusion check)
  • Missing rate limiting on password change endpoint (brute force vulnerability)
  • Potential LDAP injection via unsanitized username input
  • Missing security headers (CSP, HSTS, X-Frame-Options)
  • Plaintext secrets in environment variables

Risk Distribution

Severity Count Action Required
CRITICAL 2 Immediate (24-48 hours)
HIGH 3 Within 30 days
MEDIUM 5 Within 90 days
LOW 3 Ongoing improvement
INFO 8 Best practices

Critical Findings (Immediate Action Required)

🔴 CRITICAL-01: Case Sensitivity Bug in Username Inclusion Validation

CVSS Score: 7.5 (High) CWE: CWE-178 (Improper Handling of Case Sensitivity) OWASP Top 10: A07:2021 - Identification and Authentication Failures

Affected Files:

  • internal/rpchandler/change_password.go:64
  • internal/web/static/js/validators.ts:170

Description: The password validation logic that prevents passwords from containing the username uses case-sensitive comparison in the password change flow, while the password reset flow correctly uses case-insensitive comparison. This inconsistency allows users to bypass frontend validation by manipulating case.

Vulnerable Code:

// change_password.go:64 - VULNERABLE (case-sensitive)
if !c.opts.PasswordCanIncludeUsername && strings.Contains(sAMAccountName, newPassword) {
    return nil, fmt.Errorf("the new password must not include the username")
}

// reset_password.go:73 - CORRECT (case-insensitive)
if !h.opts.PasswordCanIncludeUsername &&
   strings.Contains(strings.ToLower(newPassword), strings.ToLower(token.Username)) {
    return nil, fmt.Errorf("the new password must not include the username")
}

Exploit Scenario:

  1. User "admin" attempts to set password "Admin123!"
  2. Frontend check (case-sensitive) allows it
  3. Backend check (case-sensitive) allows it
  4. Password contains username despite policy
  5. Violates organizational password policies

Impact:

  • Password policy bypass
  • Weakened password strength
  • Compliance violations (if policy mandates username exclusion)

Remediation: Apply case-insensitive comparison in both locations:

// Fix for change_password.go:64
if !c.opts.PasswordCanIncludeUsername &&
   strings.Contains(strings.ToLower(newPassword), strings.ToLower(sAMAccountName)) {
    return nil, fmt.Errorf("the new password must not include the username")
}
// Fix for validators.ts:170
export const mustNotIncludeUsername = (fieldName: string) => (v: string) => {
  const passwordInput = form.querySelector<HTMLInputElement>(`#username input`);
  return v.toLowerCase().includes(passwordInput.value.toLowerCase())
    ? `${fieldName} must not include the username`
    : "";
};

Verification:

# Test case 1: Same case
curl -X POST http://localhost:3000/api/rpc \
  -d '{"method":"change-password","params":["admin","OldPass1!","admin123!"]}'
# Expected: Error "must not include username"

# Test case 2: Different case (currently bypasses)
curl -X POST http://localhost:3000/api/rpc \
  -d '{"method":"change-password","params":["admin","OldPass1!","Admin123!"]}'
# Expected after fix: Error "must not include username"

🔴 CRITICAL-02: Missing Rate Limiting on Password Change Endpoint

CVSS Score: 8.1 (High) CWE: CWE-307 (Improper Restriction of Excessive Authentication Attempts) OWASP Top 10: A07:2021 - Identification and Authentication Failures

Affected Files:

  • internal/rpchandler/change_password.go (no rate limiter applied)
  • main.go (handler registration)

Description: The password change endpoint /api/rpc (method: "change-password") has no rate limiting protection, while the password reset endpoint correctly implements rate limiting. This allows unlimited password change attempts, enabling brute force attacks and denial of service.

Current Implementation:

  • Password reset request: ✅ Rate limited (5 requests per 15 minutes)
  • Password change: ❌ No rate limiting

Attack Vectors:

  1. Brute Force Attack:
# Attacker can attempt unlimited passwords
for pass in $(cat wordlist.txt); do
  curl -X POST http://localhost:3000/api/rpc \
    -d "{\"method\":\"change-password\",\"params\":[\"victim\",\"$pass\",\"NewPass123!\"]}"
done
  1. Credential Stuffing:
# Test leaked credentials from other breaches
while read line; do
  user=$(echo $line | cut -d: -f1)
  pass=$(echo $line | cut -d: -f2)
  curl -X POST http://localhost:3000/api/rpc \
    -d "{\"method\":\"change-password\",\"params\":[\"$user\",\"$pass\",\"Pwned123!\"]}"
done < leaked-passwords.txt
  1. Denial of Service:
# Exhaust LDAP connection pool
for i in {1..10000}; do
  curl -X POST http://localhost:3000/api/rpc \
    -d '{"method":"change-password","params":["user","wrong","pass"]}' &
done

Impact:

  • High: Brute force password cracking
  • High: Credential stuffing attacks
  • Medium: LDAP server resource exhaustion
  • Medium: Application availability degradation

Remediation:

Apply the existing rate limiter to the password change endpoint:

// In main.go, modify handler registration
app.Post("/api/rpc", func(c *fiber.Ctx) error {
    var body struct {
        Method string   `json:"method"`
        Params []string `json:"params"`
    }

    if err := c.BodyParser(&body); err != nil {
        return c.Status(400).JSON(fiber.Map{"error": "invalid request"})
    }

    // Apply rate limiting for authentication-sensitive methods
    if body.Method == "change-password" || body.Method == "request-password-reset" {
        identifier := c.IP() // Use IP-based rate limiting
        if rateLimiter != nil && !rateLimiter.AllowRequest(identifier) {
            return c.Status(429).JSON(rpc.JSONRPCResponse{
                Success: false,
                Data:    []string{"too many requests, please try again later"},
            })
        }
    }

    return handler.Handle(c)
})

Alternative: Per-User Rate Limiting

// Use username instead of IP for more targeted protection
identifier := body.Params[0] // username from params

Recommended Configuration:

  • Limit: 5 attempts per 15 minutes per IP/user
  • Window: Sliding window (already implemented in ratelimit/limiter.go)
  • Response: HTTP 429 Too Many Requests

Verification:

# Test rate limiting (should block after 5 attempts)
for i in {1..10}; do
  echo "Attempt $i:"
  curl -X POST http://localhost:3000/api/rpc \
    -d '{"method":"change-password","params":["test","wrong","pass"]}'
  sleep 1
done
# Expected: First 5 attempts return 500, attempts 6-10 return 429

High Severity Findings (Address Within 30 Days)

🟠 HIGH-01: Potential LDAP Injection

CVSS Score: 7.2 (High) CWE: CWE-90 (Improper Neutralization of Special Elements used in an LDAP Query) OWASP Top 10: A03:2021 - Injection

Affected Files:

  • internal/rpchandler/change_password.go:18-76
  • internal/rpchandler/reset_password.go:18-129
  • Dependency: github.com/netresearch/simple-ldap-go v1.6.0

Description: The sAMAccountName parameter is used directly in LDAP operations without explicit sanitization for LDAP special characters. The security depends entirely on the simple-ldap-go library's internal escaping mechanisms, which have not been verified.

LDAP Special Characters:

( ) * \ NUL = , + " < > ; # /

Potential Exploit:

# LDAP filter injection attempt
curl -X POST http://localhost:3000/api/rpc \
  -d '{"method":"change-password","params":["admin)(objectClass=*)","oldpass","newpass"]}'

# If simple-ldap-go doesn't escape properly, this could:
# 1. Bypass authentication checks
# 2. Enumerate LDAP directory structure
# 3. Modify unintended LDAP objects

Risk Assessment:

  • If simple-ldap-go escapes properly: LOW risk
  • If simple-ldap-go doesn't escape: CRITICAL risk

Remediation:

Option 1: Verify simple-ldap-go Security (Recommended First Step)

// Add test to verify LDAP injection protection
func TestLDAPInjectionProtection(t *testing.T) {
    maliciousUsernames := []string{
        "admin)(objectClass=*)",
        "user*",
        "admin\\*",
        "user,ou=test",
    }

    for _, username := range maliciousUsernames {
        // Attempt LDAP operation and verify it fails safely
    }
}

Option 2: Add Input Validation Layer

// In change_password.go, add before LDAP operations
func validateLDAPInput(input string) error {
    // Whitelist approach: only allow alphanumeric, underscore, hyphen, period
    matched, _ := regexp.MatchString(`^[a-zA-Z0-9._-]+$`, input)
    if !matched {
        return fmt.Errorf("invalid characters in username")
    }
    return nil
}

// Apply in changePassword function:
if err := validateLDAPInput(sAMAccountName); err != nil {
    return nil, fmt.Errorf("the username contains invalid characters")
}

Option 3: Explicit Escaping

import "strings"

func escapeLDAPSpecialChars(input string) string {
    replacements := map[string]string{
        "\\": "\\5c",
        "*":  "\\2a",
        "(":  "\\28",
        ")":  "\\29",
        "\x00": "\\00",
        "/":  "\\2f",
    }

    result := input
    for char, escape := range replacements {
        result = strings.ReplaceAll(result, char, escape)
    }
    return result
}

Immediate Action:

  1. Review simple-ldap-go source code for DN escaping
  2. Add integration tests with malicious input
  3. Implement input validation as defense-in-depth

🟠 HIGH-02: Missing Security Headers

CVSS Score: 6.5 (Medium) CWE: CWE-16 (Configuration) OWASP Top 10: A05:2021 - Security Misconfiguration

Affected Files:

  • main.go:29-50 (Fiber configuration)

Description: The application does not implement any HTTP security headers, leaving it vulnerable to multiple client-side attacks including XSS, clickjacking, and MIME sniffing.

Missing Headers:

Header Purpose Risk if Missing
Content-Security-Policy (CSP) XSS prevention HIGH - Allows inline scripts from attacker
X-Frame-Options Clickjacking prevention MEDIUM - Application can be framed
Strict-Transport-Security (HSTS) HTTPS enforcement MEDIUM - No HTTPS guarantee
X-Content-Type-Options MIME sniffing prevention LOW - Content type confusion
Referrer-Policy Referrer leakage control LOW - Information disclosure
Permissions-Policy Feature access control LOW - Unnecessary feature access

Current Vulnerability:

# Check missing headers
curl -I http://localhost:3000/ | grep -E "Content-Security-Policy|X-Frame|Strict-Transport"
# Output: (none found)

Attack Scenarios:

1. Clickjacking Attack:

<!-- Attacker's malicious site -->
<iframe src="https://gopherpass.example.com" style="opacity:0.1"> </iframe>
<button style="position:absolute; top:100px; left:100px">Click here for free prize!</button>
<!-- User clicks attacker's button, actually submits password change -->

2. XSS Exploitation (if XSS found): Without CSP, any XSS vulnerability becomes fully exploitable:

// Injected script can:
<script>
  fetch('/api/rpc', {
    method: 'POST',
    body: JSON.stringify({
      method: 'change-password',
      params: [username, currentPassword, 'AttackerPassword123!']
    })
  });
</script>

Remediation:

Add Fiber helmet middleware with comprehensive security headers:

// Install: go get github.com/gofiber/helmet/v2

import (
    "github.com/gofiber/fiber/v2"
    "github.com/gofiber/helmet/v2"
)

func main() {
    app := fiber.New(fiber.Config{
        AppName:   "netresearch/ldap-selfservice-password-changer",
        BodyLimit: 4 * 1024,
    })

    // Add security headers middleware
    app.Use(helmet.New(helmet.Config{
        // Content Security Policy
        ContentSecurityPolicy: "default-src 'self'; " +
            "script-src 'self'; " +
            "style-src 'self' 'unsafe-inline'; " + // Tailwind requires inline styles
            "img-src 'self' data:; " +
            "font-src 'self'; " +
            "connect-src 'self'; " +
            "frame-ancestors 'none'; " +
            "base-uri 'self'; " +
            "form-action 'self'",

        // Clickjacking protection
        XFrameOptions: "DENY",

        // HTTPS enforcement (only in production)
        HSTSMaxAge:            31536000, // 1 year
        HSTSIncludeSubdomains: true,
        HSTSPreload:           true,

        // MIME sniffing protection
        XContentTypeOptions: "nosniff",

        // Referrer policy
        ReferrerPolicy: "strict-origin-when-cross-origin",

        // Permissions policy
        PermissionsPolicy: "geolocation=(), microphone=(), camera=()",
    }))

    // Existing middleware...
    app.Use(compress.New(compress.Config{
        Level: compress.LevelBestSpeed,
    }))

    // ... rest of configuration
}

Environment-Specific Configuration:

// Only enable HSTS in production with HTTPS
isProduction := os.Getenv("ENVIRONMENT") == "production"

app.Use(helmet.New(helmet.Config{
    HSTSMaxAge:            func() int { if isProduction { return 31536000 }; return 0 }(),
    HSTSExcludeSubdomains: !isProduction,
}))

Verification:

# After implementing, verify headers are present
curl -I https://gopherpass.example.com/

# Expected headers:
# Content-Security-Policy: default-src 'self'; script-src 'self'; ...
# X-Frame-Options: DENY
# Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
# X-Content-Type-Options: nosniff
# Referrer-Policy: strict-origin-when-cross-origin

Testing:

# Test clickjacking protection
curl -I http://localhost:3000/ | grep X-Frame-Options
# Expected: X-Frame-Options: DENY

# Test CSP
curl -I http://localhost:3000/ | grep Content-Security-Policy
# Expected: Content-Security-Policy: default-src 'self'; ...

🟠 HIGH-03: Plaintext Secrets in Environment Variables

CVSS Score: 6.8 (Medium) CWE: CWE-256 (Plaintext Storage of a Password) OWASP Top 10: A02:2021 - Cryptographic Failures

Affected Files:

  • internal/options/app.go:1-169
  • .env files (if present)
  • Deployment configurations

Description: All sensitive credentials (LDAP passwords, SMTP password, reset user password) are stored in plaintext environment variables without encryption, rotation mechanisms, or secrets management integration.

Exposed Secrets:

# .env example (plaintext)
LDAP_PASSWORD=SuperSecret123!
SMTP_PASSWORD=MailSecret456!
RESET_USER_PASSWORD=ResetSecret789!
READONLY_USER_PASSWORD=ReadSecret012!

Attack Vectors:

1. Process Environment Access:

# Any process with access can read environment
cat /proc/<pid>/environ | tr '\0' '\n' | grep PASSWORD

2. Container Inspection:

# Docker environment variables visible
docker inspect <container-id> | grep -i password

3. Log File Exposure:

# If environment dumped to logs
docker logs <container-id> | grep PASSWORD

4. Backup/Configuration Exposure:

# .env files in backups or version control
git log --all --full-history -- ".env"

Impact:

  • High: Full LDAP administrative access if RESET_USER_PASSWORD compromised
  • High: Email system compromise if SMTP_PASSWORD compromised
  • Medium: Read access to LDAP directory if READONLY_USER_PASSWORD compromised

Remediation Options:

Option 1: Kubernetes Secrets (Recommended for K8s Deployments)

# kubernetes-secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: gopherpass-secrets
type: Opaque
stringData:
  ldap-password: "" # Set via kubectl create secret
  smtp-password: ""
  reset-user-password: ""
---
# deployment.yaml
env:
  - name: LDAP_PASSWORD
    valueFrom:
      secretKeyRef:
        name: gopherpass-secrets
        key: ldap-password

Option 2: HashiCorp Vault Integration

import (
    vault "github.com/hashicorp/vault/api"
)

func loadSecretsFromVault() (*Opts, error) {
    client, err := vault.NewClient(&vault.Config{
        Address: os.Getenv("VAULT_ADDR"),
    })
    if err != nil {
        return nil, err
    }

    client.SetToken(os.Getenv("VAULT_TOKEN"))

    secret, err := client.Logical().Read("secret/data/gopherpass")
    if err != nil {
        return nil, err
    }

    return &Opts{
        LDAPPassword: secret.Data["ldap_password"].(string),
        SMTPPassword: secret.Data["smtp_password"].(string),
        // ...
    }, nil
}

Option 3: Docker Secrets (Swarm)

func loadDockerSecret(secretName string) (string, error) {
    data, err := os.ReadFile(fmt.Sprintf("/run/secrets/%s", secretName))
    if err != nil {
        return "", err
    }
    return strings.TrimSpace(string(data)), nil
}

// In options/app.go
opts.LDAPPassword = loadDockerSecret("ldap_password")

Option 4: AWS Secrets Manager

import (
    "github.com/aws/aws-sdk-go/aws/session"
    "github.com/aws/aws-sdk-go/service/secretsmanager"
)

func loadAWSSecret(secretID string) (string, error) {
    sess := session.Must(session.NewSession())
    svc := secretsmanager.New(sess)

    input := &secretsmanager.GetSecretValueInput{
        SecretId: aws.String(secretID),
    }

    result, err := svc.GetSecretValue(input)
    if err != nil {
        return "", err
    }

    return *result.SecretString, nil
}

Immediate Action (Short-term):

# 1. Restrict .env file permissions
chmod 600 .env
chown app:app .env

# 2. Ensure .env is in .gitignore
echo ".env" >> .gitignore
echo ".env.local" >> .gitignore
echo ".env.*.local" >> .gitignore

# 3. Remove from git history if committed
git filter-branch --force --index-filter \
  "git rm --cached --ignore-unmatch .env" \
  --prune-empty --tag-name-filter cat -- --all

Long-term Action:

  1. Choose secrets management solution based on deployment environment
  2. Implement secret rotation policy (90-day rotation recommended)
  3. Add secret scanning to CI/CD pipeline (e.g., GitGuardian, TruffleHog)
  4. Document secret management procedures

Verification:

# Verify secrets not in git
git log --all --full-history -- ".env" ".env.local"
# Expected: No commits

# Verify file permissions
ls -la .env
# Expected: -rw------- (600)

# Verify secrets not in container environment
docker inspect <container> | grep -i password
# Expected: No plaintext passwords visible

Medium Severity Findings

🟡 MEDIUM-01: Information Disclosure via LDAP Error Messages

CVSS Score: 5.3 (Medium) CWE: CWE-209 (Generation of Error Message Containing Sensitive Information)

Description: Raw LDAP error messages are returned to clients, potentially exposing internal infrastructure details.

Location:

  • change_password.go:72 - return nil, err returns raw LDAP error
  • reset_password.go:123 - return nil, err returns raw LDAP error

Example Exposed Information:

LDAP Result Code 49 "Invalid Credentials"
LDAP Result Code 53 "Unwilling to Perform"
DN: CN=Users,DC=example,DC=com

Remediation:

// Wrap LDAP errors with generic messages
if err := ldap.ChangePassword(...); err != nil {
    log.Error("LDAP password change failed",
        slog.String("user", sAMAccountName),
        slog.String("error", err.Error()))
    return nil, fmt.Errorf("failed to change password, please verify your current password and try again")
}

🟡 MEDIUM-02: No Maximum Password Length Validation

CVSS Score: 5.0 (Medium) CWE: CWE-1284 (Improper Validation of Specified Quantity in Input)

Description: No maximum password length enforced, potentially causing LDAP buffer issues or DoS.

Location: internal/validators/validate.go, validators.ts

Remediation:

// Add to validation chain
const MaxPasswordLength = 128 // LDAP typical limit

if len(newPassword) > MaxPasswordLength {
    return nil, fmt.Errorf("the new password must not exceed %d characters", MaxPasswordLength)
}

🟡 MEDIUM-03: In-Memory Token Store Without Persistence

CVSS Score: 4.5 (Medium) CWE: CWE-619 (Dangling Database Cursor)

Description: Reset tokens stored in memory are lost on application restart, breaking active reset flows.

Location: internal/resettoken/store.go

Remediation Options:

  1. Redis-backed token store
  2. PostgreSQL token table
  3. Document limitation and restart implications

🟡 MEDIUM-04: No Automated Vulnerability Scanning

CVSS Score: 4.0 (Medium) CWE: CWE-1104 (Use of Unmaintained Third Party Components)

Description: No automated dependency vulnerability scanning in development or CI/CD.

Remediation:

# .github/workflows/security.yml
name: Security Scan
on: [push, pull_request]
jobs:
  govulncheck:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-go@v4
        with:
          go-version: "1.25"
      - run: go install golang.org/x/vuln/cmd/govulncheck@latest
      - run: govulncheck ./...

🟡 MEDIUM-05: No CSRF Protection

CVSS Score: 4.3 (Medium) CWE: CWE-352 (Cross-Site Request Forgery)

Description: No CSRF tokens, though risk reduced by stateless authentication requiring credentials per request.

Remediation:

// Add CSRF middleware from Fiber
import "github.com/gofiber/fiber/v2/middleware/csrf"

app.Use(csrf.New(csrf.Config{
    KeyLookup:      "header:X-CSRF-Token",
    CookieName:     "csrf_",
    CookieSameSite: "Strict",
    Expiration:     1 * time.Hour,
}))

Low Severity Findings

🔵 LOW-01: No Email Length Validation

Location: request_password_reset.go:40

Remediation:

if len(email) > 254 { // RFC 5321 maximum
    return genericSuccess, nil // Still return generic success
}

🔵 LOW-02: No TLS/HTTPS Enforcement

Description: Application runs HTTP, expects reverse proxy for TLS.

Remediation: Document HTTPS requirement in deployment guide.


🔵 LOW-03: No Connection Validation on Startup

Description: LDAP/SMTP connections not tested during initialization.

Remediation:

func validateConnections(opts *Opts) error {
    // Test LDAP connection
    if err := ldap.TestConnection(opts); err != nil {
        return fmt.Errorf("LDAP connection failed: %w", err)
    }

    // Test SMTP connection
    if err := smtp.TestConnection(opts); err != nil {
        return fmt.Errorf("SMTP connection failed: %w", err)
    }

    return nil
}

Positive Security Findings

✅ Excellent Cryptographic Practices

  1. Token Generation: Uses crypto/rand (cryptographically secure)
  2. Token Size: 256-bit tokens (43-character base64 URL-safe)
  3. No Weak Algorithms: No MD5, SHA1, or weak crypto found

Code Reference:

// internal/resettoken/token.go:8-19
bytes := make([]byte, 32) // 256 bits
_, err := rand.Read(bytes)
token := base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(bytes)

✅ Docker Security Best Practices

  1. Scratch Base Image: Minimal attack surface (no shell, no package manager)
  2. Non-Root Execution: User 65534:65534 (nobody:nogroup)
  3. Static Binary: No dynamic linking vulnerabilities
  4. Pinned Images: SHA256 hashes prevent supply chain attacks
  5. Multi-Stage Build: Build tools not in runtime image

Code Reference:

# Dockerfile:39-48
FROM scratch AS runner
USER 65534:65534
COPY --from=backend-builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=backend-builder /build/ldap-passwd /ldap-selfservice-password-changer

✅ Stateless Authentication Design

  • No session cookies = No session fixation/hijacking
  • No JWT = No token vulnerabilities
  • No persistent authentication state = Reduced attack window

✅ User Enumeration Prevention

Password reset flow never reveals if user exists:

// request_password_reset.go:104
genericSuccess := []string{"If an account exists, a reset email has been sent"}
return genericSuccess, nil // Always returns same message

✅ Defense in Depth

  1. Dual Validation: Frontend (UX) + Backend (security)
  2. Rate Limiting: Implemented for password reset (needs extension to password change)
  3. Body Size Limits: 4KB prevents large payload attacks
  4. Structured Logging: slog with PII protection

✅ Thread Safety

Token store uses proper mutex synchronization:

// internal/resettoken/store.go:14-18
type Store struct {
    mu     sync.RWMutex
    tokens map[string]*ResetToken
}

✅ Single-Use Token Enforcement

Tokens are deleted after use, preventing replay attacks:

// reset_password.go:55-60
token, exists := h.tokenStore.Get(resetToken)
if !exists || token.IsExpired() {
    return nil, fmt.Errorf("invalid or expired reset token")
}
h.tokenStore.Delete(resetToken) // Single-use enforcement

OWASP Top 10 2021 Compliance Matrix

Category Status Findings
A01: Broken Access Control ⚠️ PARTIAL Missing rate limiting on password change
A02: Cryptographic Failures ⚠️ PARTIAL Excellent crypto, but plaintext secrets in env
A03: Injection ⚠️ PARTIAL Potential LDAP injection (needs verification)
A04: Insecure Design ✅ PASS Stateless design, defense in depth
A05: Security Misconfiguration ❌ FAIL Missing security headers, no CORS
A06: Vulnerable Components ⚠️ UNKNOWN No automated scanning
A07: Auth Failures ❌ FAIL Case sensitivity bug, no rate limiting
A08: Integrity Failures ✅ PASS Go modules with checksums, pinned images
A09: Logging Failures ✅ PASS Structured logging, no PII leakage
A10: SSRF ✅ N/A No URL fetching functionality

Overall Compliance: 50% (5/10 categories passing)


Prioritized Remediation Roadmap

Phase 1: Critical Fixes (Week 1)

Timeline: Complete within 48-72 hours Effort: 8-12 developer hours

Priority Issue Effort Impact
1 Fix case sensitivity bug (CRITICAL-01) 2 hours Immediate security improvement
2 Add rate limiting to password change (CRITICAL-02) 4 hours Prevents brute force attacks
3 Verify LDAP injection protection (HIGH-01) 6 hours Critical if library doesn't escape

Deliverables:

  • Case-insensitive username comparison in change_password.go and validators.ts
  • Rate limiter applied to password change endpoint
  • LDAP injection tests written and passing
  • Hotfix deployed to production

Phase 2: High Severity Fixes (Weeks 2-4)

Timeline: Complete within 30 days Effort: 16-24 developer hours

Priority Issue Effort Impact
4 Implement security headers (HIGH-02) 4 hours Comprehensive client-side protection
5 Secrets management integration (HIGH-03) 12-20 hours Protects all credentials

Deliverables:

  • Fiber helmet middleware with CSP, HSTS, X-Frame-Options
  • Kubernetes Secrets or Vault integration for credentials
  • Secret rotation documentation and procedures
  • Security header testing suite

Phase 3: Medium Severity Fixes (Weeks 5-12)

Timeline: Complete within 90 days Effort: 24-32 developer hours

Priority Issue Effort Impact
6 Wrap LDAP errors (MEDIUM-01) 4 hours Prevents information disclosure
7 Add max password length (MEDIUM-02) 2 hours DoS prevention
8 Automated vuln scanning (MEDIUM-04) 4 hours Continuous security monitoring
9 CSRF protection (MEDIUM-05) 6 hours Defense in depth
10 Persistent token store (MEDIUM-03) 8-12 hours Improved reliability

Deliverables:

  • Generic LDAP error wrapping with detailed logging
  • Password length validation (max 128 characters)
  • GitHub Actions workflow with govulncheck
  • CSRF middleware implementation
  • Redis or PostgreSQL token storage

Phase 4: Ongoing Improvements (Continuous)

Timeline: Ongoing process improvements Effort: 2-4 hours per month

Priority Issue Effort Impact
11 Email length validation (LOW-01) 1 hour Input validation completeness
12 HTTPS documentation (LOW-02) 2 hours Deployment security guidance
13 Connection validation (LOW-03) 3 hours Improved error reporting
14 Security monitoring 2 hours/month Threat detection

Deliverables:

  • Complete input validation for all fields
  • Deployment security guide with TLS requirements
  • Startup connection health checks
  • Security monitoring dashboards

Testing Recommendations

Security Test Suite

# 1. Test case sensitivity fix
go test -v ./internal/rpchandler -run TestPasswordUsernameInclusion

# 2. Test rate limiting
go test -v ./internal/ratelimit -run TestRateLimitPasswordChange

# 3. Test LDAP injection protection
go test -v ./internal/rpchandler -run TestLDAPInjectionProtection

# 4. Test security headers
curl -I http://localhost:3000/ | grep -E "Content-Security|X-Frame|HSTS"

# 5. Run vulnerability scan
govulncheck ./...

# 6. Test CSRF protection
curl -X POST http://localhost:3000/api/rpc \
  -H "Content-Type: application/json" \
  -d '{"method":"change-password","params":["test","old","new"]}'

Penetration Testing Scenarios

  1. Brute Force Testing:

    • Attempt 100 password changes in 1 minute
    • Expected: Rate limit after 5 attempts
  2. LDAP Injection Testing:

    • Submit usernames with LDAP special characters
    • Expected: Sanitization or safe escaping
  3. Clickjacking Testing:

    • Embed application in iframe
    • Expected: X-Frame-Options blocks framing
  4. CSRF Testing:

    • Submit password change from external origin
    • Expected: CSRF token validation failure

Monitoring and Alerting Recommendations

Security Metrics to Track

# Prometheus metrics recommendations
- gopherpass_authentication_failures_total{endpoint}
- gopherpass_rate_limit_triggers_total{ip,endpoint}
- gopherpass_password_change_attempts_total{result}
- gopherpass_password_reset_requests_total{result}
- gopherpass_ldap_errors_total{type}
- gopherpass_token_store_size

Alerting Rules

# Alert on excessive authentication failures
- alert: HighAuthenticationFailureRate
  expr: rate(gopherpass_authentication_failures_total[5m]) > 10
  annotations:
    summary: High authentication failure rate detected

# Alert on rate limit triggers
- alert: RateLimitTriggered
  expr: rate(gopherpass_rate_limit_triggers_total[1m]) > 5
  annotations:
    summary: Multiple rate limit triggers (possible attack)

# Alert on LDAP connection errors
- alert: LDAPConnectionErrors
  expr: rate(gopherpass_ldap_errors_total{type="connection"}[5m]) > 0
  annotations:
    summary: LDAP connection errors detected

Security Maintenance Schedule

Daily

  • Review authentication failure logs
  • Monitor rate limit triggers
  • Check LDAP connection health

Weekly

  • Review security alerts and anomalies
  • Update dependencies with security patches
  • Review user access patterns

Monthly

  • Run govulncheck vulnerability scan
  • Review and rotate LDAP service account passwords
  • Audit security logs for patterns

Quarterly

  • Comprehensive security assessment
  • Penetration testing
  • Security training for development team
  • Review and update security documentation

Annually

  • External security audit
  • OWASP Top 10 compliance review
  • Threat model update
  • Disaster recovery testing

References and Resources

OWASP Resources

CVE Databases

Tools

Best Practices


Conclusion

GopherPass demonstrates strong foundational security with excellent cryptographic practices, secure container configuration, and thoughtful authentication design. However, 5 critical and high-severity findings require immediate attention to prevent exploitation:

  1. Case sensitivity bug in password validation (CRITICAL-01)
  2. Missing rate limiting on password change (CRITICAL-02)
  3. Potential LDAP injection (HIGH-01)
  4. Missing security headers (HIGH-02)
  5. Plaintext secrets in environment (HIGH-03)

Immediate Actions (This Week):

  • Fix case sensitivity bug (2 hours)
  • Apply rate limiting to password change (4 hours)
  • Verify LDAP injection protection (6 hours)

Following the prioritized remediation roadmap will elevate the security posture from MODERATE to STRONG within 90 days.


Assessment Conducted By: Claude (Security Analysis Agent) Report Generated: 2025-10-09 Next Review Recommended: 2025-11-09 (30 days)


This report is confidential and intended solely for the GopherPass development team. Distribution outside the team requires security team approval.