mirror of https://github.com/dexidp/dex.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
301 lines
8.5 KiB
301 lines
8.5 KiB
|
3 months ago
|
package server
|
||
|
|
|
||
|
|
import (
|
||
|
|
"bytes"
|
||
|
|
"io"
|
||
|
|
"net/http"
|
||
|
|
"net/http/httptest"
|
||
|
|
"strings"
|
||
|
|
"testing"
|
||
|
|
|
||
|
|
"github.com/stretchr/testify/require"
|
||
|
|
)
|
||
|
|
|
||
|
|
// TestErrorMessagesDoNotLeakInternalDetails verifies that error responses
|
||
|
|
// do not contain internal error details that could be exploited by attackers.
|
||
|
|
func TestErrorMessagesDoNotLeakInternalDetails(t *testing.T) {
|
||
|
|
// List of sensitive patterns that should never appear in user-facing errors
|
||
|
|
sensitivePatterns := []string{
|
||
|
|
"panic",
|
||
|
|
"runtime error",
|
||
|
|
"nil pointer",
|
||
|
|
"stack trace",
|
||
|
|
"goroutine",
|
||
|
|
".go:", // file paths like "server.go:123"
|
||
|
|
"sql:", // SQL errors
|
||
|
|
"connection", // Connection errors
|
||
|
|
"timeout", // Unless it's a user-friendly timeout message
|
||
|
|
"ECONNREFUSED",
|
||
|
|
"EOF",
|
||
|
|
"broken pipe",
|
||
|
|
}
|
||
|
|
|
||
|
|
tests := []struct {
|
||
|
|
name string
|
||
|
|
path string
|
||
|
|
method string
|
||
|
|
body string
|
||
|
|
contentType string
|
||
|
|
setupFunc func(t *testing.T, s *Server)
|
||
|
|
checkFunc func(t *testing.T, resp *http.Response, body string)
|
||
|
|
}{
|
||
|
|
{
|
||
|
|
name: "Invalid authorization request parse error",
|
||
|
|
path: "/auth",
|
||
|
|
method: "POST",
|
||
|
|
body: "invalid%body",
|
||
|
|
contentType: "application/x-www-form-urlencoded",
|
||
|
|
checkFunc: func(t *testing.T, resp *http.Response, body string) {
|
||
|
|
// Should return a safe error message, not the parse error details
|
||
|
|
for _, pattern := range sensitivePatterns {
|
||
|
|
require.NotContains(t, body, pattern,
|
||
|
|
"Response should not contain sensitive pattern: %s", pattern)
|
||
|
|
}
|
||
|
|
},
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: "Invalid callback state",
|
||
|
|
path: "/callback?state=invalid_state",
|
||
|
|
method: "GET",
|
||
|
|
checkFunc: func(t *testing.T, resp *http.Response, body string) {
|
||
|
|
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||
|
|
// Should not leak storage error details
|
||
|
|
require.NotContains(t, body, "storage")
|
||
|
|
require.NotContains(t, body, "not found")
|
||
|
|
},
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: "Invalid token request",
|
||
|
|
path: "/token",
|
||
|
|
method: "POST",
|
||
|
|
body: "grant_type=authorization_code&code=invalid",
|
||
|
|
contentType: "application/x-www-form-urlencoded",
|
||
|
|
checkFunc: func(t *testing.T, resp *http.Response, body string) {
|
||
|
|
// Token endpoint returns JSON errors which is correct OAuth2 behavior
|
||
|
|
// Just verify no internal details leak
|
||
|
|
for _, pattern := range sensitivePatterns {
|
||
|
|
require.NotContains(t, body, pattern,
|
||
|
|
"Response should not contain sensitive pattern: %s", pattern)
|
||
|
|
}
|
||
|
|
},
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: "Invalid introspection request - no token",
|
||
|
|
path: "/token/introspect",
|
||
|
|
method: "POST",
|
||
|
|
body: "",
|
||
|
|
contentType: "application/x-www-form-urlencoded",
|
||
|
|
checkFunc: func(t *testing.T, resp *http.Response, body string) {
|
||
|
|
for _, pattern := range sensitivePatterns {
|
||
|
|
require.NotContains(t, body, pattern,
|
||
|
|
"Response should not contain sensitive pattern: %s", pattern)
|
||
|
|
}
|
||
|
|
},
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: "Device flow invalid user code",
|
||
|
|
path: "/device/auth/verify_code",
|
||
|
|
method: "POST",
|
||
|
|
body: "user_code=INVALID",
|
||
|
|
checkFunc: func(t *testing.T, resp *http.Response, body string) {
|
||
|
|
for _, pattern := range sensitivePatterns {
|
||
|
|
require.NotContains(t, body, pattern,
|
||
|
|
"Response should not contain sensitive pattern: %s", pattern)
|
||
|
|
}
|
||
|
|
},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
for _, tc := range tests {
|
||
|
|
t.Run(tc.name, func(t *testing.T) {
|
||
|
|
httpServer, s := newTestServer(t, nil)
|
||
|
|
defer httpServer.Close()
|
||
|
|
|
||
|
|
if tc.setupFunc != nil {
|
||
|
|
tc.setupFunc(t, s)
|
||
|
|
}
|
||
|
|
|
||
|
|
var reqBody io.Reader
|
||
|
|
if tc.body != "" {
|
||
|
|
reqBody = strings.NewReader(tc.body)
|
||
|
|
}
|
||
|
|
|
||
|
|
req := httptest.NewRequest(tc.method, tc.path, reqBody)
|
||
|
|
if tc.contentType != "" {
|
||
|
|
req.Header.Set("Content-Type", tc.contentType)
|
||
|
|
}
|
||
|
|
|
||
|
|
rr := httptest.NewRecorder()
|
||
|
|
s.ServeHTTP(rr, req)
|
||
|
|
|
||
|
|
resp := rr.Result()
|
||
|
|
defer resp.Body.Close()
|
||
|
|
|
||
|
|
bodyBytes, err := io.ReadAll(resp.Body)
|
||
|
|
require.NoError(t, err)
|
||
|
|
body := string(bodyBytes)
|
||
|
|
|
||
|
|
if tc.checkFunc != nil {
|
||
|
|
tc.checkFunc(t, resp, body)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestLoginErrorMessageIsSafe verifies that the login error page
|
||
|
|
// shows a safe, user-friendly message.
|
||
|
|
func TestLoginErrorMessageIsSafe(t *testing.T) {
|
||
|
|
httpServer, s := newTestServer(t, nil)
|
||
|
|
defer httpServer.Close()
|
||
|
|
|
||
|
|
// Create a request that will trigger a login error
|
||
|
|
rr := httptest.NewRecorder()
|
||
|
|
req := httptest.NewRequest("GET", "/auth/nonexistent/login?state=test", nil)
|
||
|
|
s.ServeHTTP(rr, req)
|
||
|
|
|
||
|
|
resp := rr.Result()
|
||
|
|
defer resp.Body.Close()
|
||
|
|
|
||
|
|
body, _ := io.ReadAll(resp.Body)
|
||
|
|
bodyStr := string(body)
|
||
|
|
|
||
|
|
// Should not contain error stack traces or internal details
|
||
|
|
require.NotContains(t, bodyStr, "panic")
|
||
|
|
require.NotContains(t, bodyStr, ".go:")
|
||
|
|
require.NotContains(t, bodyStr, "goroutine")
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestCallbackErrorMessageIsSafe verifies that callback errors
|
||
|
|
// do not leak internal details.
|
||
|
|
func TestCallbackErrorMessageIsSafe(t *testing.T) {
|
||
|
|
httpServer, s := newTestServer(t, nil)
|
||
|
|
defer httpServer.Close()
|
||
|
|
|
||
|
|
// Test OAuth2 callback with invalid state
|
||
|
|
rr := httptest.NewRecorder()
|
||
|
|
req := httptest.NewRequest("GET", "/callback?code=test&state=invalid", nil)
|
||
|
|
s.ServeHTTP(rr, req)
|
||
|
|
|
||
|
|
resp := rr.Result()
|
||
|
|
defer resp.Body.Close()
|
||
|
|
|
||
|
|
body, _ := io.ReadAll(resp.Body)
|
||
|
|
bodyStr := string(body)
|
||
|
|
|
||
|
|
// Should not contain storage error details
|
||
|
|
require.NotContains(t, bodyStr, "storage.ErrNotFound")
|
||
|
|
require.NotContains(t, bodyStr, "database")
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestDeviceCallbackMethodError verifies that unsupported methods
|
||
|
|
// return safe error messages.
|
||
|
|
func TestDeviceCallbackMethodError(t *testing.T) {
|
||
|
|
httpServer, s := newTestServer(t, nil)
|
||
|
|
defer httpServer.Close()
|
||
|
|
|
||
|
|
// Test with unsupported method
|
||
|
|
rr := httptest.NewRecorder()
|
||
|
|
req := httptest.NewRequest("PUT", "/device/callback", nil)
|
||
|
|
s.ServeHTTP(rr, req)
|
||
|
|
|
||
|
|
resp := rr.Result()
|
||
|
|
defer resp.Body.Close()
|
||
|
|
|
||
|
|
body, _ := io.ReadAll(resp.Body)
|
||
|
|
bodyStr := string(body)
|
||
|
|
|
||
|
|
// Should not expose the method name in error
|
||
|
|
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||
|
|
require.NotContains(t, bodyStr, "PUT")
|
||
|
|
require.NotContains(t, bodyStr, "method not implemented")
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestRenderErrorSafeMessages tests that renderError uses safe messages
|
||
|
|
func TestRenderErrorSafeMessages(t *testing.T) {
|
||
|
|
tests := []struct {
|
||
|
|
name string
|
||
|
|
statusCode int
|
||
|
|
message string
|
||
|
|
expectedInBody []string
|
||
|
|
notInBody []string
|
||
|
|
}{
|
||
|
|
{
|
||
|
|
name: "Login error message",
|
||
|
|
statusCode: http.StatusInternalServerError,
|
||
|
|
message: ErrMsgLoginError,
|
||
|
|
expectedInBody: []string{"Login error", "administrator"},
|
||
|
|
notInBody: []string{"stack", "panic", ".go:"},
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: "Authentication failed message",
|
||
|
|
statusCode: http.StatusInternalServerError,
|
||
|
|
message: ErrMsgAuthenticationFailed,
|
||
|
|
expectedInBody: []string{"Authentication failed", "administrator"},
|
||
|
|
notInBody: []string{"stack", "panic", ".go:"},
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: "Database error message",
|
||
|
|
statusCode: http.StatusInternalServerError,
|
||
|
|
message: ErrMsgDatabaseError,
|
||
|
|
expectedInBody: []string{"database error"},
|
||
|
|
notInBody: []string{"sql:", "connection", "timeout"},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
for _, tc := range tests {
|
||
|
|
t.Run(tc.name, func(t *testing.T) {
|
||
|
|
httpServer, s := newTestServer(t, nil)
|
||
|
|
defer httpServer.Close()
|
||
|
|
|
||
|
|
rr := httptest.NewRecorder()
|
||
|
|
req := httptest.NewRequest("GET", "/", nil)
|
||
|
|
|
||
|
|
s.renderError(req, rr, tc.statusCode, tc.message)
|
||
|
|
|
||
|
|
resp := rr.Result()
|
||
|
|
defer resp.Body.Close()
|
||
|
|
|
||
|
|
body, _ := io.ReadAll(resp.Body)
|
||
|
|
bodyStr := string(body)
|
||
|
|
|
||
|
|
require.Equal(t, tc.statusCode, resp.StatusCode)
|
||
|
|
|
||
|
|
for _, expected := range tc.expectedInBody {
|
||
|
|
require.Contains(t, bodyStr, expected,
|
||
|
|
"Response should contain: %s", expected)
|
||
|
|
}
|
||
|
|
|
||
|
|
for _, notExpected := range tc.notInBody {
|
||
|
|
require.NotContains(t, bodyStr, notExpected,
|
||
|
|
"Response should not contain: %s", notExpected)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestTokenErrorDoesNotLeakDetails tests that token errors don't leak internal details
|
||
|
|
func TestTokenErrorDoesNotLeakDetails(t *testing.T) {
|
||
|
|
httpServer, s := newTestServer(t, nil)
|
||
|
|
defer httpServer.Close()
|
||
|
|
|
||
|
|
// Create a token request with invalid credentials
|
||
|
|
body := bytes.NewBufferString("grant_type=authorization_code&code=invalid_code")
|
||
|
|
req := httptest.NewRequest("POST", "/token", body)
|
||
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||
|
|
req.SetBasicAuth("invalid_client", "invalid_secret")
|
||
|
|
|
||
|
|
rr := httptest.NewRecorder()
|
||
|
|
s.ServeHTTP(rr, req)
|
||
|
|
|
||
|
|
resp := rr.Result()
|
||
|
|
defer resp.Body.Close()
|
||
|
|
|
||
|
|
respBody, _ := io.ReadAll(resp.Body)
|
||
|
|
bodyStr := string(respBody)
|
||
|
|
|
||
|
|
// Should not contain internal error details
|
||
|
|
require.NotContains(t, bodyStr, "storage")
|
||
|
|
require.NotContains(t, bodyStr, "not found")
|
||
|
|
require.NotContains(t, bodyStr, ".go:")
|
||
|
|
}
|