Some checks failed
🚀 Build & Deploy Image / Determine Build Necessity (push) Failing after 10m14s
🚀 Build & Deploy Image / Build Runtime Base Image (push) Has been skipped
🚀 Build & Deploy Image / Build Docker Image (push) Has been skipped
🚀 Build & Deploy Image / Run Tests & Quality Checks (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Staging (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Production (push) Has been skipped
Security Vulnerability Scan / Check for Dependency Changes (push) Failing after 11m25s
Security Vulnerability Scan / Composer Security Audit (push) Has been cancelled
- Remove middleware reference from Gitea Traefik labels (caused routing issues) - Optimize Gitea connection pool settings (MAX_IDLE_CONNS=30, authentication_timeout=180s) - Add explicit service reference in Traefik labels - Fix intermittent 504 timeouts by improving PostgreSQL connection handling Fixes Gitea unreachability via git.michaelschiemer.de
255 lines
6.6 KiB
JavaScript
255 lines
6.6 KiB
JavaScript
/**
|
|
* Security Manager
|
|
*
|
|
* Provides security-related utilities including CSRF, XSS protection, and CSP helpers.
|
|
*/
|
|
|
|
import { Logger } from '../../core/logger.js';
|
|
import { CsrfManager } from './CsrfManager.js';
|
|
|
|
/**
|
|
* SecurityManager - Centralized security utilities
|
|
*/
|
|
export class SecurityManager {
|
|
constructor(config = {}) {
|
|
this.config = {
|
|
csrf: config.csrf || {},
|
|
xss: {
|
|
enabled: config.xss?.enabled ?? true,
|
|
sanitizeOnInput: config.xss?.sanitizeOnInput ?? false
|
|
},
|
|
csp: {
|
|
enabled: config.csp?.enabled ?? false,
|
|
reportOnly: config.csp?.reportOnly ?? false
|
|
},
|
|
...config
|
|
};
|
|
|
|
this.csrfManager = null;
|
|
|
|
// Initialize
|
|
this.init();
|
|
}
|
|
|
|
/**
|
|
* Create a new SecurityManager instance
|
|
*/
|
|
static create(config = {}) {
|
|
return new SecurityManager(config);
|
|
}
|
|
|
|
/**
|
|
* Initialize security manager
|
|
*/
|
|
init() {
|
|
// Initialize CSRF manager
|
|
this.csrfManager = CsrfManager.create(this.config.csrf);
|
|
|
|
// Initialize XSS protection if enabled
|
|
if (this.config.xss.enabled) {
|
|
this.initXssProtection();
|
|
}
|
|
|
|
// Initialize CSP if enabled
|
|
if (this.config.csp.enabled) {
|
|
this.initCsp();
|
|
}
|
|
|
|
Logger.info('[SecurityManager] Initialized', {
|
|
csrf: !!this.csrfManager,
|
|
xss: this.config.xss.enabled,
|
|
csp: this.config.csp.enabled
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Initialize XSS protection
|
|
*/
|
|
initXssProtection() {
|
|
// Add input sanitization if enabled
|
|
if (this.config.xss.sanitizeOnInput) {
|
|
document.addEventListener('input', (event) => {
|
|
if (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') {
|
|
this.sanitizeInput(event.target);
|
|
}
|
|
}, true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sanitize input value
|
|
*/
|
|
sanitizeInput(element) {
|
|
const originalValue = element.value;
|
|
const sanitized = this.sanitizeHtml(originalValue);
|
|
|
|
if (sanitized !== originalValue) {
|
|
element.value = sanitized;
|
|
Logger.warn('[SecurityManager] Sanitized potentially dangerous input', {
|
|
field: element.name || element.id
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sanitize HTML string
|
|
*/
|
|
sanitizeHtml(html) {
|
|
if (typeof html !== 'string') {
|
|
return html;
|
|
}
|
|
|
|
// Remove script tags and event handlers
|
|
return html
|
|
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
|
|
.replace(/on\w+\s*=\s*["'][^"']*["']/gi, '')
|
|
.replace(/javascript:/gi, '')
|
|
.replace(/data:text\/html/gi, '');
|
|
}
|
|
|
|
/**
|
|
* Initialize Content Security Policy
|
|
*/
|
|
initCsp() {
|
|
// CSP is typically set server-side, but we can validate it client-side
|
|
const cspHeader = this.getCspHeader();
|
|
|
|
if (cspHeader) {
|
|
Logger.debug('[SecurityManager] CSP header found', cspHeader);
|
|
this.validateCsp(cspHeader);
|
|
} else {
|
|
Logger.warn('[SecurityManager] No CSP header found');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get CSP header from meta tag or response headers
|
|
*/
|
|
getCspHeader() {
|
|
// Try meta tag
|
|
const metaTag = document.querySelector('meta[http-equiv="Content-Security-Policy"]');
|
|
if (metaTag) {
|
|
return metaTag.getAttribute('content');
|
|
}
|
|
|
|
// Note: Response headers are not accessible from JavaScript
|
|
// This would need to be passed from server or checked server-side
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Validate CSP header
|
|
*/
|
|
validateCsp(cspHeader) {
|
|
// Basic validation - check for common security directives
|
|
const requiredDirectives = ['default-src', 'script-src', 'style-src'];
|
|
const directives = cspHeader.split(';').map(d => d.trim().split(' ')[0]);
|
|
|
|
const missing = requiredDirectives.filter(dir =>
|
|
!directives.some(d => d.toLowerCase() === dir.toLowerCase())
|
|
);
|
|
|
|
if (missing.length > 0) {
|
|
Logger.warn('[SecurityManager] CSP missing recommended directives', missing);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get CSRF token
|
|
*/
|
|
getCsrfToken() {
|
|
return this.csrfManager?.getToken() || null;
|
|
}
|
|
|
|
/**
|
|
* Get CSRF token header
|
|
*/
|
|
getCsrfTokenHeader() {
|
|
return this.csrfManager?.getTokenHeader() || {};
|
|
}
|
|
|
|
/**
|
|
* Refresh CSRF token
|
|
*/
|
|
async refreshCsrfToken() {
|
|
if (this.csrfManager) {
|
|
return await this.csrfManager.refreshToken();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate security headers
|
|
*/
|
|
validateSecurityHeaders() {
|
|
const issues = [];
|
|
|
|
// Check for HTTPS
|
|
if (location.protocol !== 'https:' && location.hostname !== 'localhost') {
|
|
issues.push('Not using HTTPS');
|
|
}
|
|
|
|
// Check for CSP
|
|
if (!this.getCspHeader()) {
|
|
issues.push('No Content Security Policy header');
|
|
}
|
|
|
|
// Check for X-Frame-Options
|
|
// Note: Headers are not accessible from JavaScript, would need server-side check
|
|
|
|
return {
|
|
valid: issues.length === 0,
|
|
issues
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Escape HTML to prevent XSS
|
|
*/
|
|
escapeHtml(text) {
|
|
if (typeof text !== 'string') {
|
|
return text;
|
|
}
|
|
|
|
const map = {
|
|
'&': '&',
|
|
'<': '<',
|
|
'>': '>',
|
|
'"': '"',
|
|
"'": '''
|
|
};
|
|
|
|
return text.replace(/[&<>"']/g, m => map[m]);
|
|
}
|
|
|
|
/**
|
|
* Validate URL to prevent XSS
|
|
*/
|
|
validateUrl(url) {
|
|
try {
|
|
const parsed = new URL(url, window.location.origin);
|
|
|
|
// Block javascript: and data: URLs
|
|
if (parsed.protocol === 'javascript:' || parsed.protocol === 'data:') {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Destroy security manager
|
|
*/
|
|
destroy() {
|
|
if (this.csrfManager) {
|
|
this.csrfManager.destroy();
|
|
this.csrfManager = null;
|
|
}
|
|
|
|
Logger.info('[SecurityManager] Destroyed');
|
|
}
|
|
}
|
|
|