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:
@@ -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[:])
|
|
||||||
}
|
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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}</>;
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ 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);
|
||||||
@@ -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,27 +198,6 @@ 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">
|
||||||
|
|||||||
Reference in New Issue
Block a user