521 lines
18 KiB
Markdown
521 lines
18 KiB
Markdown
# API Security Audit Report
|
|
|
|
**Repository**: guardrail-mcp
|
|
**Component**: mcp-server/internal/web/
|
|
**Audit Date**: 2026-02-08
|
|
**Auditor**: Security Engineer
|
|
|
|
## Executive Summary
|
|
|
|
This audit covers the web layer of the guardrail-mcp server, focusing on authentication, authorization, rate limiting, CORS, input validation, and security headers. **3 Critical, 5 High, 4 Medium, and 3 Low severity issues** were identified.
|
|
|
|
### Risk Overview
|
|
| Severity | Count | Immediate Action Required |
|
|
|----------|-------|--------------------------|
|
|
| Critical | 3 | Yes |
|
|
| High | 5 | Yes |
|
|
| Medium | 4 | Recommended |
|
|
| Low | 3 | Scheduled |
|
|
|
|
---
|
|
|
|
## Critical Issues
|
|
|
|
### 1. AUTH-BYPASS-001: Path Traversal in Authentication Bypass
|
|
**File**: `/mnt/ollama/git/agent-guardrails-template/mcp-server/internal/web/middleware.go:21-28`
|
|
|
|
**Issue**: The authentication middleware uses `c.Path()` to check if routes should be skipped, but `c.Path()` returns the *route pattern* (e.g., `/health/live`), not the actual request path. A request to `/health/live/../../../api/sensitive` would match the `/health/live` pattern, bypassing authentication.
|
|
|
|
```go
|
|
// VULNERABLE CODE
|
|
path := c.Path() // Returns route pattern, not actual URL
|
|
if path == "/health/live" || path == "/health/ready" || path == "/metrics" {
|
|
return next(c) // Bypasses auth incorrectly
|
|
}
|
|
```
|
|
|
|
**Severity**: Critical
|
|
**CVSS**: 9.1 (Critical)
|
|
**Impact**: Complete authentication bypass, unauthorized API access
|
|
**Remediation**:
|
|
```go
|
|
path := c.Request().URL.Path // Use actual request path
|
|
// Normalize path to prevent traversal
|
|
path = filepath.Clean(path)
|
|
|
|
// Use exact matching with proper path handling
|
|
if path == "/health/live" || path == "/health/ready" || path == "/metrics" {
|
|
return next(c)
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 2. RATE-001: Rate Limiting Bypass via IP Spoofing
|
|
**File**: `/mnt/ollama/git/agent-guardrails-template/mcp-server/internal/web/middleware.go:105-108`
|
|
|
|
**Issue**: When API key hash is not available, the middleware falls back to `c.RealIP()` for rate limiting. Echo's `RealIP()` extracts IP from `X-Forwarded-For`/`X-Real-IP` headers without validation, allowing attackers to spoof different IPs and bypass rate limits.
|
|
|
|
```go
|
|
// VULNERABLE CODE
|
|
keyHash, ok := c.Get("api_key_hash").(string)
|
|
if !ok {
|
|
keyHash = c.RealIP() // Easily spoofed via headers
|
|
}
|
|
```
|
|
|
|
**Severity**: Critical
|
|
**CVSS**: 8.2 (High)
|
|
**Impact**: Rate limit bypass, potential DoS
|
|
**Remediation**:
|
|
```go
|
|
// Extract IP from trusted source only
|
|
func getClientIP(c echo.Context, trustProxy bool) string {
|
|
if !trustProxy {
|
|
// Use direct connection IP
|
|
ip := c.Request().RemoteAddr
|
|
host, _, err := net.SplitHostPort(ip)
|
|
if err == nil {
|
|
return host
|
|
}
|
|
return ip
|
|
}
|
|
// Only trust proxy headers if behind verified load balancer
|
|
return c.RealIP()
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 3. CORS-001: Overly Permissive CORS in Non-Production
|
|
**File**: `/mnt/ollama/git/agent-guardrails-template/mcp-server/internal/web/server.go:87-102`
|
|
|
|
**Issue**: In non-production mode, CORS allows `http://localhost:*` and `https://localhost:*`, which permits any localhost port. This allows malicious websites running on localhost to make authenticated cross-origin requests.
|
|
|
|
```go
|
|
// VULNERABLE CODE - allows any localhost port
|
|
corsOrigins = []string{"http://localhost:*", "https://localhost:*"}
|
|
```
|
|
|
|
**Severity**: Critical
|
|
**CVSS**: 7.5 (High)
|
|
**Impact**: CSRF-style attacks from malicious localhost services
|
|
**Remediation**:
|
|
```go
|
|
// Use specific allowed origins only
|
|
corsOrigins = s.cfg.CORSAllowedOrigins
|
|
// Validate origins against allowlist
|
|
allowedOrigins := map[string]bool{
|
|
"http://localhost:3000": true,
|
|
"http://localhost:8081": true,
|
|
// Add specific allowed origins
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## High Severity Issues
|
|
|
|
### 4. INPUT-001: Insufficient Input Validation on PATCH Endpoints
|
|
**File**: `/mnt/ollama/git/agent-guardrails-template/mcp-server/internal/web/handlers.go:283-343`
|
|
|
|
**Issue**: The `patchRule` handler accepts arbitrary fields in the request body without strict validation. While the struct defines expected fields, Echo's `Bind()` method will ignore unknown fields, potentially allowing injection of unexpected data.
|
|
|
|
**Severity**: High
|
|
**Impact**: Mass assignment vulnerability, data corruption
|
|
**Remediation**:
|
|
```go
|
|
// Use strict validation
|
|
var req struct {
|
|
Enabled *bool `json:"enabled,omitempty" validate:"omitempty"`
|
|
Name *string `json:"name,omitempty" validate:"omitempty,max=255"`
|
|
Message *string `json:"message,omitempty" validate:"omitempty,max=1000"`
|
|
Pattern *string `json:"pattern,omitempty" validate:"omitempty,max=500"`
|
|
Severity *string `json:"severity,omitempty" validate:"omitempty,oneof=low medium high critical"`
|
|
}
|
|
|
|
// Add strict decoding to reject unknown fields
|
|
decoder := json.NewDecoder(c.Request().Body)
|
|
decoder.DisallowUnknownFields()
|
|
if err := decoder.Decode(&req); err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid fields in request"})
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 5. INPUT-002: Missing Content-Type Validation
|
|
**Files**: All handlers in `/mnt/ollama/git/agent-guardrails-template/mcp-server/internal/web/handlers.go`
|
|
|
|
**Issue**: No Content-Type validation on requests. Endpoints accepting JSON don't verify `Content-Type: application/json`, allowing CSRF attacks through HTML forms or content type confusion attacks.
|
|
|
|
**Severity**: High
|
|
**Impact**: CSRF attacks, content type confusion
|
|
**Remediation**:
|
|
```go
|
|
// Add middleware for content type validation
|
|
func RequireContentType(contentTypes ...string) echo.MiddlewareFunc {
|
|
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
|
return func(c echo.Context) error {
|
|
if c.Request().Method == http.MethodGet || c.Request().Method == http.MethodDelete {
|
|
return next(c)
|
|
}
|
|
contentType := c.Request().Header.Get("Content-Type")
|
|
for _, allowed := range contentTypes {
|
|
if strings.HasPrefix(contentType, allowed) {
|
|
return next(c)
|
|
}
|
|
}
|
|
return echo.NewHTTPError(http.StatusUnsupportedMediaType, "invalid content type")
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 6. AUTH-002: Weak IDE Endpoint Authorization Logic
|
|
**File**: `/mnt/ollama/git/agent-guardrails-template/mcp-server/internal/web/middleware.go:59-62`
|
|
|
|
**Issue**: The endpoint restriction logic has a logical error. It checks if path starts with `/ide` and requires `ide` key type, but `mcp` key type is also allowed, making the restriction ineffective.
|
|
|
|
```go
|
|
// INEFFECTIVE CODE
|
|
if strings.HasPrefix(path, "/ide") && keyType != "ide" && keyType != "mcp" {
|
|
return echo.NewHTTPError(http.StatusForbidden, "IDE API key required")
|
|
}
|
|
```
|
|
|
|
**Severity**: High
|
|
**Impact**: Authorization bypass, privilege escalation
|
|
**Remediation**:
|
|
```go
|
|
// Proper endpoint authorization
|
|
switch {
|
|
case strings.HasPrefix(path, "/ide"):
|
|
if keyType != "ide" {
|
|
return echo.NewHTTPError(http.StatusForbidden, "IDE API key required")
|
|
}
|
|
case strings.HasPrefix(path, "/api/admin"):
|
|
if keyType != "admin" { // Add admin key type
|
|
return echo.NewHTTPError(http.StatusForbidden, "admin key required")
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 7. SEC-001: Missing HSTS Header
|
|
**File**: `/mnt/ollama/git/agent-guardrails-template/mcp-server/internal/web/server.go:246-270`
|
|
|
|
**Issue**: The security headers middleware doesn't include `Strict-Transport-Security` (HSTS), allowing downgrade attacks when TLS is enabled.
|
|
|
|
**Severity**: High
|
|
**Impact**: SSL/TLS downgrade attacks, MITM
|
|
**Remediation**:
|
|
```go
|
|
func securityHeadersMiddleware() echo.MiddlewareFunc {
|
|
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
|
return func(c echo.Context) error {
|
|
// Add HSTS when TLS is enabled
|
|
if c.Request().TLS != nil {
|
|
c.Response().Header().Set(
|
|
"Strict-Transport-Security",
|
|
"max-age=31536000; includeSubDomains; preload",
|
|
)
|
|
}
|
|
// ... existing headers ...
|
|
return next(c)
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 8. ERROR-001: Verbose Error Messages in Production
|
|
**Files**: Multiple handlers in `/mnt/ollama/git/agent-guardrails-template/mcp-server/internal/web/handlers.go`
|
|
|
|
**Issue**: Error messages expose internal details (e.g., database errors, file paths) in production. Example: `return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})`
|
|
|
|
**Severity**: High
|
|
**Impact**: Information disclosure, system fingerprinting
|
|
**Remediation**:
|
|
```go
|
|
func handleError(c echo.Context, err error, publicMsg string, isProduction bool) error {
|
|
slog.Error("Request failed", "error", err, "path", c.Path())
|
|
if isProduction {
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{
|
|
"error": publicMsg,
|
|
"request_id": c.Response().Header().Get("X-Request-ID"),
|
|
})
|
|
}
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Medium Severity Issues
|
|
|
|
### 9. INPUT-003: Insufficient Query Parameter Validation
|
|
**File**: `/mnt/ollama/git/agent-guardrails-template/mcp-server/internal/web/handlers.go:116-140`
|
|
|
|
**Issue**: Search query parameter `q` is accepted without length limits or sanitization, potentially enabling ReDoS (Regular Expression Denial of Service) if regex is used in search.
|
|
|
|
**Severity**: Medium
|
|
**Impact**: ReDoS, resource exhaustion
|
|
**Remediation**:
|
|
```go
|
|
const maxQueryLength = 200
|
|
|
|
func (s *Server) searchDocuments(c echo.Context) error {
|
|
query := c.QueryParam("q")
|
|
if query == "" {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "query required"})
|
|
}
|
|
if len(query) > maxQueryLength {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{
|
|
"error": "query too long",
|
|
"max_length": strconv.Itoa(maxQueryLength),
|
|
})
|
|
}
|
|
// Sanitize query to prevent regex injection
|
|
query = sanitizeSearchQuery(query)
|
|
// ... rest of handler
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 10. RATE-002: No Burst Protection for Rate Limiting
|
|
**File**: `/mnt/ollama/git/agent-guardrails-template/mcp-server/internal/cache/redis.go:282-298`
|
|
|
|
**Issue**: The rate limiter uses a simple counter without burst bucket or proper sliding window implementation. It resets at window boundaries, allowing burst attacks at minute boundaries.
|
|
|
|
**Severity**: Medium
|
|
**Impact**: Burst attacks at window boundaries
|
|
**Remediation**:
|
|
```go
|
|
// Implement token bucket or proper sliding window
|
|
func (dl *DistributedRateLimiter) Allow(ctx context.Context, key string, limit int, burst int) bool {
|
|
now := time.Now().UnixMilli()
|
|
bucketKey := fmt.Sprintf("ratelimit:bucket:%s", key)
|
|
|
|
lua := `
|
|
local key = KEYS[1]
|
|
local now = tonumber(ARGV[1])
|
|
local limit = tonumber(ARGV[2])
|
|
local burst = tonumber(ARGV[3])
|
|
local window = tonumber(ARGV[4])
|
|
|
|
local bucket = redis.call('hmget', key, 'tokens', 'last_update')
|
|
local tokens = tonumber(bucket[1]) or burst
|
|
local last_update = tonumber(bucket[2]) or now
|
|
|
|
local delta = math.max(0, now - last_update)
|
|
tokens = math.min(burst, tokens + (delta * limit / window))
|
|
|
|
if tokens < 1 then
|
|
return 0
|
|
end
|
|
|
|
tokens = tokens - 1
|
|
redis.call('hset', key, 'tokens', tokens, 'last_update', now)
|
|
redis.call('pexpire', key, window)
|
|
return 1
|
|
`
|
|
|
|
result, err := dl.redis.Eval(ctx, lua, []string{bucketKey}, now, limit, burst, 60000).Int()
|
|
if err != nil {
|
|
slog.Error("Rate limiter error", "error", err)
|
|
return false // Fail closed
|
|
}
|
|
return result == 1
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 11. SEC-002: Version Endpoint Information Disclosure
|
|
**File**: `/mnt/ollama/git/agent-guardrails-template/mcp-server/internal/web/server.go:273-280`
|
|
|
|
**Issue**: The `/version` endpoint exposes version information without authentication, enabling attackers to identify vulnerable versions.
|
|
|
|
**Severity**: Medium
|
|
**Impact**: System fingerprinting, targeted attacks
|
|
**Remediation**:
|
|
```go
|
|
// Require authentication for version endpoint
|
|
// Or return minimal information
|
|
func (s *Server) versionInfo(c echo.Context) error {
|
|
// Only return version to authenticated requests
|
|
if c.Get("api_key_type") == nil {
|
|
return echo.NewHTTPError(http.StatusUnauthorized, "authentication required")
|
|
}
|
|
// ... existing code ...
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 12. SEC-003: Missing Request Size Validation for Query Parameters
|
|
**File**: `/mnt/ollama/git/agent-guardrails-template/mcp-server/internal/web/server.go:109-110`
|
|
|
|
**Issue**: While body size is limited to 10MB, there's no validation on URL length or query string size, potentially enabling HTTP request smuggling or buffer overflow attacks.
|
|
|
|
**Severity**: Medium
|
|
**Impact**: HTTP request smuggling, DoS
|
|
**Remediation**:
|
|
```go
|
|
// Add URL length validation middleware
|
|
func URLLengthLimit(maxLength int) echo.MiddlewareFunc {
|
|
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
|
return func(c echo.Context) error {
|
|
if len(c.Request().URL.String()) > maxLength {
|
|
return echo.NewHTTPError(http.StatusRequestURITooLong, "URL too long")
|
|
}
|
|
return next(c)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Use in setupMiddleware
|
|
s.echo.Use(URLLengthLimit(4096))
|
|
```
|
|
|
|
---
|
|
|
|
## Low Severity Issues
|
|
|
|
### 13. SEC-004: X-XSS-Protection Deprecated Header
|
|
**File**: `/mnt/ollama/git/agent-guardrails-template/mcp-server/internal/web/server.go:264`
|
|
|
|
**Issue**: The `X-XSS-Protection` header is deprecated and can introduce vulnerabilities in older browsers. CSP is the modern replacement.
|
|
|
|
**Severity**: Low
|
|
**Remediation**:
|
|
```go
|
|
// Remove X-XSS-Protection header
|
|
// CSP already provides better protection
|
|
```
|
|
|
|
---
|
|
|
|
### 14. LOG-001: Sensitive Information in Logs (API Key Hash)
|
|
**File**: `/mnt/ollama/git/agent-guardrails-template/mcp-server/internal/web/middleware.go:66-72`
|
|
|
|
**Issue**: API key hashes are logged, which could enable offline cracking attempts if logs are compromised.
|
|
|
|
**Severity**: Low
|
|
**Impact**: Information disclosure (requires log access)
|
|
**Remediation**:
|
|
```go
|
|
// Don't log key hashes, only use for rate limiting internally
|
|
// Or use HMAC with server secret for log identifiers
|
|
```
|
|
|
|
---
|
|
|
|
### 15. SEC-005: Race Condition in Cache Invalidation
|
|
**File**: `/mnt/ollama/git/agent-guardrails-template/mcp-server/internal/web/handlers.go:334-336`
|
|
|
|
**Issue**: The `patchRule` handler doesn't use the same error handling pattern as other handlers for cache invalidation, potentially masking failures.
|
|
|
|
**Severity**: Low
|
|
**Impact**: Inconsistent error handling, stale cache
|
|
**Remediation**: Standardize cache invalidation error handling across all handlers.
|
|
|
|
---
|
|
|
|
## Security Strengths
|
|
|
|
The following security controls are well-implemented:
|
|
|
|
1. **Constant-Time API Key Comparison** (`middleware.go:47-50`): Uses `subtle.ConstantTimeCompare` to prevent timing attacks
|
|
2. **Secret Scanning** (`handlers.go:90-96`): Documents are scanned for secrets before saving
|
|
3. **CSP Implementation** (`server.go:251-261`): Comprehensive Content Security Policy
|
|
4. **Panic Recovery** (`server.go:213-244`): Proper panic recovery with stack trace logging
|
|
5. **Request Timeout** (`server.go:104-107`): Configurable request timeouts
|
|
6. **Body Size Limit** (`server.go:110`): 10MB body limit prevents large payload attacks
|
|
7. **Audit Logging** (multiple files): Comprehensive audit logging for changes
|
|
8. **Health Check Security** (`server.go:296-313`): Health endpoints don't expose component details
|
|
|
|
---
|
|
|
|
## Remediation Priority
|
|
|
|
### Immediate (Critical & High - Next 7 Days)
|
|
1. [ ] Fix AUTH-BYPASS-001: Use actual request path instead of route pattern
|
|
2. [ ] Fix RATE-001: Validate proxy headers before using RealIP()
|
|
3. [ ] Fix CORS-001: Remove wildcard localhost origins
|
|
4. [ ] Fix AUTH-002: Correct IDE endpoint authorization logic
|
|
5. [ ] Fix INPUT-001: Add strict PATCH validation
|
|
6. [ ] Fix SEC-001: Add HSTS header for TLS connections
|
|
|
|
### Short-term (High & Medium - Next 30 Days)
|
|
7. [ ] Fix INPUT-002: Add Content-Type validation middleware
|
|
8. [ ] Fix ERROR-001: Implement production-safe error handling
|
|
9. [ ] Fix INPUT-003: Add query parameter length limits
|
|
10. [ ] Fix RATE-002: Implement proper token bucket rate limiting
|
|
|
|
### Long-term (Medium & Low - Next 90 Days)
|
|
11. [ ] Fix SEC-002: Protect version endpoint
|
|
12. [ ] Fix SEC-003: Add URL length validation
|
|
13. [ ] Fix SEC-004: Remove deprecated headers
|
|
14. [ ] Fix LOG-001: Reduce sensitive information in logs
|
|
15. [ ] Fix SEC-005: Standardize cache invalidation handling
|
|
|
|
---
|
|
|
|
## Testing Recommendations
|
|
|
|
1. **Authentication Bypass Testing**:
|
|
```bash
|
|
curl "http://localhost:8080/health/live/../../../api/rules" -H "Authorization: Bearer invalid"
|
|
```
|
|
|
|
2. **Rate Limit Bypass Testing**:
|
|
```bash
|
|
for i in {1..1100}; do
|
|
curl -H "X-Forwarded-For: 1.2.3.$i" http://localhost:8080/api/rules
|
|
done
|
|
```
|
|
|
|
3. **CORS Testing**:
|
|
```bash
|
|
curl -H "Origin: http://localhost:9999" -H "Authorization: Bearer $KEY" \
|
|
-X OPTIONS http://localhost:8080/api/rules -v
|
|
```
|
|
|
|
4. **Content-Type Testing**:
|
|
```bash
|
|
curl -X POST http://localhost:8080/api/rules \
|
|
-H "Content-Type: text/plain" \
|
|
-d "malicious data"
|
|
```
|
|
|
|
---
|
|
|
|
## Compliance Mapping
|
|
|
|
| Issue | OWASP Top 10 2021 | NIST 800-53 | CIS Controls |
|
|
|-------|------------------|-------------|--------------|
|
|
| AUTH-BYPASS-001 | A01:2021-Broken Access Control | AC-3 | 6.1 |
|
|
| RATE-001 | A07:2021-Identification | AC-17 | 13.1 |
|
|
| CORS-001 | A07:2021-Identification | AC-4 | 13.1 |
|
|
| INPUT-001 | A03:2021-Injection | SI-10 | 3.5 |
|
|
| SEC-001 | A02:2021-Cryptographic Failures | SC-8 | 14.4 |
|
|
|
|
---
|
|
|
|
## Appendix: File Locations
|
|
|
|
| File | Lines | Purpose |
|
|
|------|-------|---------|
|
|
| `/mnt/ollama/git/agent-guardrails-template/mcp-server/internal/web/middleware.go` | 1-133 | Authentication & rate limiting |
|
|
| `/mnt/ollama/git/agent-guardrails-template/mcp-server/internal/web/handlers.go` | 1-712 | API endpoint handlers |
|
|
| `/mnt/ollama/git/agent-guardrails-template/mcp-server/internal/web/server.go` | 1-321 | Server setup & middleware |
|
|
| `/mnt/ollama/git/agent-guardrails-template/mcp-server/internal/config/config.go` | 1-401 | Configuration & validation |
|
|
| `/mnt/ollama/git/agent-guardrails-template/mcp-server/internal/cache/redis.go` | 1-356 | Rate limiting implementation |
|