fix: critical security vulnerabilities

- Fix JWT secret derivation vulnerability - replace deriveJWTSecret with cryptographically secure GenerateSecureToken
- Secure setup interface - remove JWT secret display and API response exposure
- Addresses system-wide compromise risk from admin credential exposure
This commit is contained in:
Fimeg
2025-10-31 09:32:34 -04:00
parent e64131079e
commit 63cc7f6645
4 changed files with 33 additions and 58 deletions

View File

@@ -1,13 +1,12 @@
package handlers package handlers
import ( import (
"crypto/sha256"
"database/sql" "database/sql"
"encoding/hex"
"fmt" "fmt"
"net/http" "net/http"
"strconv" "strconv"
"github.com/Fimeg/RedFlag/aggregator-server/internal/config"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/lib/pq" "github.com/lib/pq"
_ "github.com/lib/pq" _ "github.com/lib/pq"
@@ -374,8 +373,12 @@ func (h *SetupHandler) ConfigureServer(c *gin.Context) {
return return
} }
// Generate JWT secret for display (not logged for security) // Generate secure JWT secret (not derived from credentials for security)
jwtSecret := deriveJWTSecret(req.AdminUser, req.AdminPass) jwtSecret, err := config.GenerateSecureToken()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate JWT secret"})
return
}
// Step 1: Update PostgreSQL password from bootstrap to user password // Step 1: Update PostgreSQL password from bootstrap to user password
fmt.Println("Updating PostgreSQL password from bootstrap to user-provided password...") fmt.Println("Updating PostgreSQL password from bootstrap to user-provided password...")
@@ -398,7 +401,6 @@ func (h *SetupHandler) ConfigureServer(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"message": "Configuration generated successfully!", "message": "Configuration generated successfully!",
"jwtSecret": jwtSecret,
"envContent": newEnvContent, "envContent": newEnvContent,
"restartMessage": "Please replace the bootstrap environment variables with the newly generated ones, then run: docker-compose down && docker-compose up -d", "restartMessage": "Please replace the bootstrap environment variables with the newly generated ones, then run: docker-compose down && docker-compose up -d",
"manualRestartRequired": true, "manualRestartRequired": true,
@@ -407,8 +409,3 @@ func (h *SetupHandler) ConfigureServer(c *gin.Context) {
}) })
} }
// deriveJWTSecret generates a JWT secret from admin credentials
func deriveJWTSecret(username, password string) string {
hash := sha256.Sum256([]byte(username + password + "redflag-jwt-2024"))
return hex.EncodeToString(hash[:])
}

View File

@@ -2,7 +2,6 @@ package config
import ( import (
"crypto/rand" "crypto/rand"
"crypto/sha256"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"os" "os"
@@ -123,12 +122,6 @@ func getEnv(key, defaultValue string) string {
} }
func deriveJWTSecret(username, password string) string {
// Derive JWT secret from admin credentials
// This ensures JWT secret changes if admin password changes
hash := sha256.Sum256([]byte(username + password + "redflag-jwt-2024"))
return hex.EncodeToString(hash[:])
}
// GenerateSecureToken generates a cryptographically secure random token // GenerateSecureToken generates a cryptographically secure random token
func GenerateSecureToken() (string, error) { func GenerateSecureToken() (string, error) {

View File

@@ -7,6 +7,7 @@ interface SetupCompletionCheckerProps {
} }
export const SetupCompletionChecker: React.FC<SetupCompletionCheckerProps> = ({ children }) => { export const SetupCompletionChecker: React.FC<SetupCompletionCheckerProps> = ({ children }) => {
const [wasInSetupMode, setWasInSetupMode] = useState(false);
const [isSetupMode, setIsSetupMode] = useState<boolean | null>(null); const [isSetupMode, setIsSetupMode] = useState<boolean | null>(null);
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
@@ -16,13 +17,28 @@ export const SetupCompletionChecker: React.FC<SetupCompletionCheckerProps> = ({
try { try {
const data = await setupApi.checkHealth(); const data = await setupApi.checkHealth();
if (data.status === 'waiting for configuration') { const currentSetupMode = data.status === 'waiting for configuration';
setIsSetupMode(true);
} else { // Track if we were previously in setup mode
setIsSetupMode(false); if (currentSetupMode) {
setWasInSetupMode(true);
} }
// If we were in setup mode and now we're not, redirect to login
if (wasInSetupMode && !currentSetupMode && location.pathname === '/setup') {
console.log('Setup completed - redirecting to login');
navigate('/login', { replace: true });
return; // Prevent further state updates
}
setIsSetupMode(currentSetupMode);
} catch (error) { } catch (error) {
// If we can't reach the health endpoint, assume normal mode // If we can't reach the health endpoint, assume normal mode
if (wasInSetupMode && location.pathname === '/setup') {
console.log('Setup completed (endpoint reachable) - redirecting to login');
navigate('/login', { replace: true });
return;
}
setIsSetupMode(false); setIsSetupMode(false);
} }
}; };
@@ -33,15 +49,7 @@ export const SetupCompletionChecker: React.FC<SetupCompletionCheckerProps> = ({
const interval = setInterval(checkSetupStatus, 3000); const interval = setInterval(checkSetupStatus, 3000);
return () => clearInterval(interval); return () => clearInterval(interval);
}, []); }, [wasInSetupMode, location.pathname, navigate]);
// If we're on the setup page and server is now healthy, redirect to login
useEffect(() => {
if (isSetupMode === false && location.pathname === '/setup') {
console.log('Setup completed - redirecting to login');
navigate('/login', { replace: true });
}
}, [isSetupMode, location.pathname, navigate]);
// Always render children - this component only handles redirects // Always render children - this component only handles redirects
return <>{children}</>; return <>{children}</>;

View File

@@ -21,8 +21,7 @@ const Setup: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [jwtSecret, setJwtSecret] = useState<string | null>(null); const [envContent, setEnvContent] = useState<string | null>(null);
const [envContent, setEnvContent] = useState<string | null>(null);
const [showSuccess, setShowSuccess] = useState(false); const [showSuccess, setShowSuccess] = useState(false);
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
const [showDbPassword, setShowDbPassword] = useState(false); const [showDbPassword, setShowDbPassword] = useState(false);
@@ -112,8 +111,7 @@ const Setup: React.FC = () => {
try { try {
const result = await setupApi.configure(formData); const result = await setupApi.configure(formData);
// Store JWT secret, env content and show success screen // Store env content and show success screen
setJwtSecret(result.jwtSecret || null);
setEnvContent(result.envContent || null); setEnvContent(result.envContent || null);
setShowSuccess(true); setShowSuccess(true);
toast.success(result.message || 'Configuration saved successfully!'); toast.success(result.message || 'Configuration saved successfully!');
@@ -128,8 +126,8 @@ const Setup: React.FC = () => {
} }
}; };
// Success screen with credentials display // Success screen with configuration display
if (showSuccess && jwtSecret) { if (showSuccess && envContent) {
return ( return (
<div className="px-4 sm:px-6 lg:px-8"> <div className="px-4 sm:px-6 lg:px-8">
<div className="max-w-3xl mx-auto"> <div className="max-w-3xl mx-auto">
@@ -200,28 +198,7 @@ const Setup: React.FC = () => {
</div> </div>
)} )}
{/* JWT Secret Section (Server Configuration) */}
<div className="mb-6">
<h3 className="text-lg font-semibold text-gray-900 mb-3">Server JWT Secret</h3>
<div className="bg-gray-50 border border-gray-200 rounded-md p-4">
<code className="text-sm text-gray-800 break-all font-mono">{jwtSecret}</code>
</div>
<div className="mt-3 p-3 bg-gray-50 border border-gray-200 rounded-md">
<p className="text-sm text-gray-700">
<strong>For your information:</strong> This JWT secret is used internally by the server for session management and agent authentication. It's automatically included in the configuration file above.
</p>
</div>
<button
onClick={() => {
navigator.clipboard.writeText(jwtSecret);
toast.success('JWT secret copied to clipboard!');
}}
className="mt-3 w-full flex justify-center py-2 px-4 border border-transparent rounded-md text-sm font-medium text-white bg-gray-600 hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500"
>
Copy JWT Secret (Optional)
</button>
</div>
{/* Next Steps */} {/* Next Steps */}
<div className="border-t border-gray-200 pt-6"> <div className="border-t border-gray-200 pt-6">
<h3 className="text-lg font-semibold text-gray-900 mb-3">Next Steps</h3> <h3 className="text-lg font-semibold text-gray-900 mb-3">Next Steps</h3>