Files
notifier/docs/CORS_IMPLEMENTATION.md

256 lines
8.4 KiB
Markdown

# CORS Implementation - Security Enhancement
## Overview
This document describes the implementation of a secure CORS (Cross-Origin Resource Sharing) configuration system that replaces the previous wildcard (`*`) configuration with an explicit origin whitelist to prevent CSRF attacks.
## Implementation Summary
### 1. CORSConfig Structure (api/rest/router.go)
Created a comprehensive `CORSConfig` struct with the following fields:
```go
type CORSConfig struct {
AllowedOrigins []string // Whitelist of allowed origins
AllowedMethods []string // Allowed HTTP methods
AllowedHeaders []string // Allowed HTTP headers
AllowCredentials bool // Whether to allow credentials
MaxAge int // Cache duration for preflight responses
}
```
**Key Security Features:**
- Wildcards (`*`) are explicitly NOT supported
- Origins must be explicitly whitelisted
- Credentials can only be enabled with specific origins
### 2. CORS Middleware Implementation
The `newCORSMiddleware()` function implements:
- **Origin Validation**: Checks incoming `Origin` header against whitelist
- **Exact Match Required**: Only sets CORS headers if origin is in whitelist
- **Never Uses Wildcard**: Always returns the exact origin, never `*`
- **Preflight Handling**: Properly handles OPTIONS requests
- **Configurable Headers**: All CORS headers are configurable
**Code Location:** `api/rest/router.go:98-149`
### 3. Configuration System
#### Config Structure (internal/config/config.go)
Added `CORSConfig` to the main application configuration:
```go
type CORSConfig struct {
AllowedOrigins []string `mapstructure:"allowed_origins"`
AllowedMethods []string `mapstructure:"allowed_methods"`
AllowedHeaders []string `mapstructure:"allowed_headers"`
AllowCredentials bool `mapstructure:"allow_credentials"`
MaxAge int `mapstructure:"max_age"`
}
```
#### Default Values (internal/config/config.go:205-210)
```go
AllowedOrigins: []string{} // Empty by default
AllowedMethods: []string{"GET", "POST", "OPTIONS", "DELETE"} // Standard REST methods
AllowedHeaders: []string{"Content-Type", "Authorization"} // Common headers
AllowCredentials: false // Disabled by default
MaxAge: 3600 // 1 hour
```
### 4. Configuration Validation (internal/config/config.go:263-282)
Implemented `validateCORS()` that enforces:
1. **Wildcard Rejection**: `*` is not allowed in `allowed_origins`
2. **Origin Format Validation**: Origins must start with `http://` or `https://`
3. **Credentials Validation**: `allow_credentials` requires at least one origin
**Example Error Messages:**
- `"wildcard (*) is not allowed in CORS allowed_origins for security reasons - specify exact origins instead"`
- `"invalid origin format: example.com - origins must start with http:// or https://"`
- `"allow_credentials is enabled but no origins are allowed - this configuration is ineffective"`
### 5. Updated Server Initialization (cmd/server/main.go:288-326)
The `startRESTServer()` function now:
1. Converts config CORS to `rest.CORSConfig`
2. Logs CORS configuration on startup
3. Warns if no origins are configured
4. Passes CORS config to router
**Example Log Output:**
```
CORS enabled for origins: [http://localhost:3000 http://localhost:8080]
```
Or:
```
CORS has no allowed origins configured - all cross-origin requests will be blocked
```
### 6. Configuration Example (config.yaml:112-146)
#### Development Configuration
```yaml
cors:
allowed_origins:
- "http://localhost:3000" # React/Next.js dev port
- "http://localhost:8080" # Vue/Angular dev port
- "http://localhost:5173" # Vite dev server
allowed_methods:
- "GET"
- "POST"
- "OPTIONS"
- "DELETE"
allowed_headers:
- "Content-Type"
- "Authorization"
allow_credentials: false
max_age: 3600
```
#### Production Configuration (Example)
```yaml
cors:
allowed_origins:
- "https://app.example.com"
- "https://dashboard.example.com"
- "https://api-docs.example.com"
allowed_methods:
- "GET"
- "POST"
- "OPTIONS"
- "DELETE"
allowed_headers:
- "Content-Type"
- "Authorization"
allow_credentials: true # Enable for auth tokens
max_age: 3600
```
## Test Coverage
### 1. CORS Middleware Tests (api/rest/cors_test.go)
**Test Cases:**
-`TestCORSMiddleware_AllowedOrigin`: Verifies allowed origins are accepted
-`TestCORSMiddleware_BlockedOrigin`: Verifies non-whitelisted origins are rejected
-`TestCORSMiddleware_PreflightRequest`: Tests OPTIONS preflight handling
-`TestCORSMiddleware_Credentials`: Verifies credential header handling
-`TestCORSMiddleware_NoWildcard`: Ensures wildcard is never returned
-`TestCORSMiddleware_EmptyConfig`: Tests secure default (no origins)
-`TestCORSMiddleware_MaxAge`: Tests cache duration configuration
-`TestDefaultCORSConfig`: Verifies default configuration values
**Total: 8 test functions, 21 test cases**
### 2. CORS Validation Tests (internal/config/cors_test.go)
**Test Cases:**
-`TestValidateCORS_WildcardRejection`: Wildcard origins are rejected
-`TestValidateCORS_InvalidOriginFormat`: Invalid origin formats are rejected
-`TestValidateCORS_CredentialsWithoutOrigins`: Credentials require origins
-`TestValidateCORS_ValidConfigurations`: Valid configs are accepted
-`TestValidateCORS_MultipleOrigins`: Multiple origins with wildcard rejected
-`TestValidateCORS_EdgeCases`: Edge cases handled correctly
**Total: 6 test functions, 16 test cases**
## Security Improvements
### Before
```go
w.Header().Set("Access-Control-Allow-Origin", "*") // ❌ CSRF Vulnerability
```
### After
```go
// Only set CORS headers if origin is in whitelist
if allowed {
w.Header().Set("Access-Control-Allow-Origin", origin) // ✅ Exact origin
}
// Otherwise, no CORS headers are set (browser blocks the response)
```
## Security Guarantees
1. **No Wildcard**: The system makes it impossible to configure wildcard CORS
2. **Explicit Whitelist**: All allowed origins must be explicitly configured
3. **Validation at Startup**: Invalid configurations are rejected before the server starts
4. **Default Secure**: Empty origin list by default (most restrictive)
5. **Environment-Specific**: Different configs for dev/staging/production
## Migration Guide
### For Development
Update your `config.yaml` to include localhost origins:
```yaml
cors:
allowed_origins:
- "http://localhost:3000"
- "http://localhost:8080"
```
### For Production
Configure your production origins:
```yaml
cors:
allowed_origins:
- "https://yourdomain.com"
- "https://app.yourdomain.com"
allow_credentials: true
```
### Environment Variables
You can also configure CORS via environment variables:
```bash
export NOTIFIER_CORS_ALLOWED_ORIGINS="https://app.example.com,https://dashboard.example.com"
export NOTIFIER_CORS_ALLOW_CREDENTIALS=true
```
## Acceptance Criteria
**CORS whitelist fully configurable** - via config.yaml or environment variables
**Wildcard configuration is impossible** - validation rejects `*` at startup
**Security headers properly set** - only for whitelisted origins
**Environment-specific configs work** - examples provided for dev/prod
**No CSRF vulnerability** - wildcard eliminated, exact origins only
**Comprehensive tests** - 37 test cases covering all scenarios
## Files Modified
1. `api/rest/router.go` - CORS config struct and middleware
2. `api/rest/cors_test.go` - CORS middleware tests (NEW)
3. `internal/config/config.go` - CORS configuration structure
4. `internal/config/cors_test.go` - CORS validation tests (NEW)
5. `cmd/server/main.go` - Server initialization with CORS config
6. `config.yaml` - Example CORS configuration
7. `internal/auth/bootstrap.go` - Fixed unrelated string formatting bug
8. `internal/auth/keystore_hybrid.go` - Fixed unrelated rate limiter signature
## Additional Fixes
While implementing CORS, also fixed pre-existing build errors:
- String multiplication in `bootstrap.go` (changed to `strings.Repeat`)
- Rate limiter signature mismatch in `keystore_hybrid.go`
- Missing parameter in `service_retention_test.go`
## References
- **OWASP CORS**: https://owasp.org/www-community/attacks/csrf
- **MDN CORS**: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
- **RFC 6454**: The Web Origin Concept