Security fixes n.a.v. pentest op noorlander.info

- CSRF token toegevoegd aan admin login formulier
- Directory listing uitgeschakeld (Options -Indexes)
- Secure/SameSite=Strict cookie verbeterd in AdminAuth
- app.js.backup en source maps verwijderd
- Version disclosure configureerbaar via config.json
- .map en backup extensies geblokkeerd in .htaccess
This commit is contained in:
2026-07-14 14:25:04 +02:00
parent 4d2e11e419
commit c6c2fdb67b
11 changed files with 46 additions and 767 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-743
View File
@@ -1,743 +0,0 @@
// Main application JavaScript for CodePress CMS
// Enhanced with PWA support and accessibility features
// Initialize application when DOM is ready
document.addEventListener('DOMContentLoaded', function() {
console.log('CodePress CMS v1.5.0 initialized');
// Register Service Worker for PWA
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(function(registration) {
console.log('Service Worker registered:', registration.scope);
})
.catch(function(error) {
console.log('Service Worker registration failed:', error);
});
}
// Handle nested dropdowns for touch devices
initializeDropdowns();
// Initialize accessibility features
initializeAccessibility();
// Initialize form validation
initializeFormValidation();
// Initialize PWA features
initializePWA();
// Initialize search enhancements
initializeSearch();
// Run accessibility tests in development
if (window.location.hostname === 'localhost') {
setTimeout(runAccessibilityTests, 1000);
}
});
// Dropdown menu handling
function initializeDropdowns() {
const dropdownSubmenus = document.querySelectorAll('.dropdown-submenu');
dropdownSubmenus.forEach(function(submenu) {
const toggle = submenu.querySelector('.dropdown-toggle');
const dropdown = submenu.querySelector('.dropdown-menu');
if (toggle && dropdown) {
// Prevent default link behavior
toggle.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
// Close other submenus at the same level
const parent = submenu.parentElement;
parent.querySelectorAll('.dropdown-submenu').forEach(function(sibling) {
if (sibling !== submenu) {
sibling.querySelector('.dropdown-menu').classList.remove('show');
}
});
// Toggle current submenu
dropdown.classList.toggle('show');
});
// Close submenu when clicking outside
document.addEventListener('click', function(e) {
if (!submenu.contains(e.target)) {
dropdown.classList.remove('show');
}
});
// Keyboard navigation for dropdowns
toggle.addEventListener('keydown', function(e) {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
dropdown.classList.toggle('show');
} else if (e.key === 'Escape') {
dropdown.classList.remove('show');
toggle.focus();
}
});
}
});
}
// Accessibility enhancements
function initializeAccessibility() {
// High contrast mode detection
if (window.matchMedia && window.matchMedia('(prefers-contrast: high)').matches) {
document.documentElement.classList.add('high-contrast');
}
// Reduced motion preference
if (window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
document.documentElement.classList.add('reduced-motion');
}
// Focus management
document.addEventListener('keydown', function(e) {
// Close modals with Escape
if (e.key === 'Escape') {
const openModals = document.querySelectorAll('.modal.show');
openModals.forEach(modal => {
const bsModal = bootstrap.Modal.getInstance(modal);
if (bsModal) bsModal.hide();
});
// Close dropdowns
const openDropdowns = document.querySelectorAll('.dropdown-menu.show');
openDropdowns.forEach(dropdown => {
dropdown.classList.remove('show');
});
}
});
// Announce dynamic content changes to screen readers
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
// Announce new content
announceToScreenReader('Content updated', 'polite');
}
});
});
observer.observe(document.body, {
childList: true,
subtree: true
});
// Focus trap for modals
document.addEventListener('shown.bs.modal', function(e) {
const modal = e.target;
trapFocus(modal);
});
document.addEventListener('hidden.bs.modal', function(e) {
const modal = e.target;
releaseFocusTrap(modal);
});
// Enhanced keyboard navigation
document.addEventListener('keydown', function(e) {
// Skip to content with Ctrl+Home
if (e.ctrlKey && e.key === 'Home') {
e.preventDefault();
const mainContent = document.getElementById('main-content');
if (mainContent) {
mainContent.focus();
mainContent.scrollIntoView();
}
}
});
}
// PWA functionality
function initializePWA() {
// Install prompt handling
let deferredPrompt;
window.addEventListener('beforeinstallprompt', function(e) {
e.preventDefault();
deferredPrompt = e;
// Show install button if desired
const installButton = document.createElement('button');
installButton.textContent = 'Install App';
installButton.className = 'btn btn-primary position-fixed bottom-0 end-0 m-3 d-none d-md-block';
installButton.style.zIndex = '1050';
installButton.addEventListener('click', function() {
deferredPrompt.prompt();
deferredPrompt.userChoice.then(function(choiceResult) {
if (choiceResult.outcome === 'accepted') {
console.log('User accepted the install prompt');
}
deferredPrompt = null;
document.body.removeChild(installButton);
});
});
document.body.appendChild(installButton);
});
// Online/offline status
window.addEventListener('online', function() {
console.log('Connection restored');
showToast('Connection restored', 'success');
});
window.addEventListener('offline', function() {
console.log('Connection lost');
showToast('You are offline', 'warning');
});
}
// Form validation and error handling
function initializeFormValidation() {
const forms = document.querySelectorAll('form');
forms.forEach(function(form) {
form.addEventListener('submit', function(e) {
if (!validateForm(form)) {
e.preventDefault();
// Focus first invalid field
const firstInvalid = form.querySelector('[aria-invalid="true"]');
if (firstInvalid) {
firstInvalid.focus();
}
}
});
// Real-time validation
const inputs = form.querySelectorAll('input, select, textarea');
inputs.forEach(function(input) {
input.addEventListener('blur', function() {
validateField(input);
});
input.addEventListener('input', function() {
// Clear errors on input
clearFieldError(input);
});
});
});
}
// Validate entire form
function validateForm(form) {
let isValid = true;
const inputs = form.querySelectorAll('input, select, textarea');
inputs.forEach(function(input) {
if (!validateField(input)) {
isValid = false;
}
});
return isValid;
}
// Validate individual field
function validateField(field) {
const value = field.value.trim();
let isValid = true;
let errorMessage = '';
// Required field validation
if (field.hasAttribute('required') && !value) {
isValid = false;
errorMessage = 'This field is required';
}
// Email validation
if (field.type === 'email' && value) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(value)) {
isValid = false;
errorMessage = 'Please enter a valid email address';
}
}
// Search field validation (minimum length)
if (field.type === 'search' && value && value.length < 2) {
isValid = false;
errorMessage = 'Please enter at least 2 characters';
}
// Update field state
field.setAttribute('aria-invalid', isValid ? 'false' : 'true');
if (!isValid) {
showFieldError(field, errorMessage);
} else {
clearFieldError(field);
}
return isValid;
}
// Show field error
function showFieldError(field, message) {
// Remove existing error
clearFieldError(field);
// Create error message
const errorDiv = document.createElement('div');
errorDiv.className = 'invalid-feedback d-block';
errorDiv.setAttribute('role', 'alert');
errorDiv.setAttribute('aria-live', 'polite');
errorDiv.textContent = message;
// Add error class to field
field.classList.add('is-invalid');
// Insert error after field
field.parentNode.insertBefore(errorDiv, field.nextSibling);
}
// Clear field error
function clearFieldError(field) {
field.classList.remove('is-invalid');
const errorDiv = field.parentNode.querySelector('.invalid-feedback');
if (errorDiv) {
errorDiv.remove();
}
}
// Enhanced search functionality
function initializeSearch() {
const searchInputs = document.querySelectorAll('input[type="search"]');
searchInputs.forEach(function(input) {
// Clear search on Escape
input.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
input.value = '';
input.blur();
announceToScreenReader('Search cleared', 'polite');
}
});
// Auto-focus search on '/' key
document.addEventListener('keydown', function(e) {
if (e.key === '/' && !['INPUT', 'TEXTAREA', 'SELECT'].includes(e.target.tagName)) {
e.preventDefault();
input.focus();
announceToScreenReader('Search input focused', 'polite');
}
});
// Announce search results
input.addEventListener('input', debounce(function() {
if (input.value.length > 0) {
announceToScreenReader(`Searching for: ${input.value}`, 'polite');
}
}, 500));
});
}
// Toast notification system
function showToast(message, type = 'info') {
// Create toast container if it doesn't exist
let toastContainer = document.querySelector('.toast-container');
if (!toastContainer) {
toastContainer = document.createElement('div');
toastContainer.className = 'toast-container position-fixed top-0 end-0 p-3';
toastContainer.style.zIndex = '1060';
document.body.appendChild(toastContainer);
}
// Create toast
const toast = document.createElement('div');
toast.className = `toast align-items-center text-white bg-${type} border-0`;
toast.setAttribute('role', 'alert');
toast.setAttribute('aria-live', 'assertive');
toast.setAttribute('aria-atomic', 'true');
toast.innerHTML = `
<div class="d-flex">
<div class="toast-body">${message}</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
`;
toastContainer.appendChild(toast);
// Initialize and show toast
const bsToast = new bootstrap.Toast(toast);
bsToast.show();
// Remove toast after it's hidden
toast.addEventListener('hidden.bs.toast', function() {
toast.remove();
});
}
// Utility functions for accessibility
function announceToScreenReader(message, priority = 'polite') {
// Remove existing announcements
const existing = document.querySelectorAll('[aria-live]');
existing.forEach(el => {
if (el !== document.querySelector('.sr-only[aria-live]')) el.remove();
});
const announcement = document.createElement('div');
announcement.setAttribute('aria-live', priority);
announcement.setAttribute('aria-atomic', 'true');
announcement.className = 'sr-only';
announcement.textContent = message;
document.body.appendChild(announcement);
// Remove after announcement
setTimeout(() => {
if (announcement.parentNode) {
document.body.removeChild(announcement);
}
}, 1000);
}
function trapFocus(element) {
const focusableElements = element.querySelectorAll(
'a[href], button, textarea, input[type="text"], input[type="search"], ' +
'input[type="email"], select, [tabindex]:not([tabindex="-1"])'
);
if (focusableElements.length === 0) return null;
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
function handleTab(e) {
if (e.key !== 'Tab') return;
if (e.shiftKey) {
if (document.activeElement === firstElement) {
e.preventDefault();
lastElement.focus();
}
} else {
if (document.activeElement === lastElement) {
e.preventDefault();
firstElement.focus();
}
}
}
element.addEventListener('keydown', handleTab);
// Focus first element
firstElement.focus();
// Return cleanup function
return function() {
element.removeEventListener('keydown', handleTab);
};
}
function releaseFocusTrap(element) {
// Focus trap is automatically released when event listener is removed
// This function can be extended for additional cleanup
}
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// Accessibility testing function
function runAccessibilityTests() {
console.log('🧪 Running Accessibility Tests...');
const results = {
passed: 0,
failed: 0,
warnings: 0,
total: 0
};
// Test 1: Check for alt text on images
const images = document.querySelectorAll('img');
images.forEach(img => {
results.total++;
if (!img.hasAttribute('alt') && !img.hasAttribute('role') && img.getAttribute('role') !== 'presentation') {
console.warn('⚠️ Image missing alt text:', img.src);
results.warnings++;
} else {
results.passed++;
}
});
// Test 2: Check for form labels
const inputs = document.querySelectorAll('input:not([type="hidden"]), select, textarea');
inputs.forEach(input => {
results.total++;
const label = document.querySelector(`label[for="${input.id}"]`);
const ariaLabel = input.getAttribute('aria-label');
const ariaLabelledBy = input.getAttribute('aria-labelledby');
if (!label && !ariaLabel && !ariaLabelledBy) {
console.error('❌ Form control missing label:', input.name || input.id);
results.failed++;
} else {
results.passed++;
}
});
// Test 3: Check heading hierarchy
const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6');
let lastLevel = 0;
headings.forEach(heading => {
results.total++;
const level = parseInt(heading.tagName.charAt(1));
if (level - lastLevel > 1 && lastLevel !== 0) {
console.warn('⚠️ Skipped heading level:', heading.textContent.trim().substring(0, 50));
results.warnings++;
} else {
results.passed++;
}
lastLevel = level;
});
// Test 4: Check ARIA landmarks
results.total++;
const landmarks = document.querySelectorAll('[role="banner"], [role="main"], [role="complementary"], [role="contentinfo"], header, main, aside, footer');
const uniqueRoles = new Set();
landmarks.forEach(element => {
const role = element.getAttribute('role') || element.tagName.toLowerCase();
uniqueRoles.add(role);
});
const requiredRoles = ['banner', 'main', 'contentinfo'];
let hasRequired = true;
requiredRoles.forEach(role => {
if (!uniqueRoles.has(role)) {
console.error(`❌ Missing ARIA landmark: ${role}`);
hasRequired = false;
}
});
if (hasRequired) {
results.passed++;
} else {
results.failed++;
}
// Test 5: Check focus indicators
results.total++;
const focusableElements = document.querySelectorAll('a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])');
if (focusableElements.length === 0) {
results.passed++;
} else {
// Check if focus styles are defined in CSS
const computedStyle = getComputedStyle(focusableElements[0]);
const outline = computedStyle.outline;
const boxShadow = computedStyle.boxShadow;
if (outline !== 'none' && outline !== '' && outline !== '0px none rgb(0, 0, 0)') {
results.passed++;
} else if (boxShadow && boxShadow !== 'none') {
results.passed++;
} else {
console.warn('⚠️ Focus indicators may not be visible');
results.warnings++;
}
}
// Summary
console.log(`\n📊 Accessibility Test Results:`);
console.log(`✅ Passed: ${results.passed}`);
console.log(`❌ Failed: ${results.failed}`);
console.log(`⚠️ Warnings: ${results.warnings}`);
console.log(`📈 Success Rate: ${Math.round((results.passed / results.total) * 100)}%`);
if (results.failed === 0 && results.warnings === 0) {
console.log('🎉 All accessibility tests passed!');
} else if (results.failed === 0) {
console.log('👍 Accessibility compliant with minor warnings');
} else {
console.log('⚠️ Accessibility issues found - review and fix');
}
return results;
}
// Utility functions for accessibility
function announceToScreenReader(message, priority = 'polite') {
// Remove existing announcements
const existing = document.querySelectorAll('[aria-live]');
existing.forEach(el => {
if (el !== document.querySelector('.sr-only[aria-live]')) el.remove();
});
const announcement = document.createElement('div');
announcement.setAttribute('aria-live', priority);
announcement.setAttribute('aria-atomic', 'true');
announcement.className = 'sr-only';
announcement.textContent = message;
document.body.appendChild(announcement);
// Remove after announcement
setTimeout(() => {
if (announcement.parentNode) {
document.body.removeChild(announcement);
}
}, 1000);
}
function releaseFocusTrap(element) {
// Focus trap is automatically released when event listener is removed
// This function can be extended for additional cleanup
}
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// Accessibility testing function
function runAccessibilityTests() {
console.log('🧪 Running Accessibility Tests...');
const results = {
passed: 0,
failed: 0,
warnings: 0,
total: 0
};
// Test 1: Check for alt text on images
const images = document.querySelectorAll('img');
images.forEach(img => {
results.total++;
if (!img.hasAttribute('alt') && !img.hasAttribute('role') && img.getAttribute('role') !== 'presentation') {
console.warn('⚠️ Image missing alt text:', img.src);
results.warnings++;
} else {
results.passed++;
}
});
// Test 2: Check for form labels
const inputs = document.querySelectorAll('input:not([type="hidden"]), select, textarea');
inputs.forEach(input => {
results.total++;
const label = document.querySelector(`label[for="${input.id}"]`);
const ariaLabel = input.getAttribute('aria-label');
const ariaLabelledBy = input.getAttribute('aria-labelledby');
if (!label && !ariaLabel && !ariaLabelledBy) {
console.error('❌ Form control missing label:', input.name || input.id);
results.failed++;
} else {
results.passed++;
}
});
// Test 3: Check heading hierarchy
const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6');
let lastLevel = 0;
headings.forEach(heading => {
results.total++;
const level = parseInt(heading.tagName.charAt(1));
if (level - lastLevel > 1 && lastLevel !== 0) {
console.warn('⚠️ Skipped heading level:', heading.textContent.trim().substring(0, 50));
results.warnings++;
} else {
results.passed++;
}
lastLevel = level;
});
// Test 4: Check ARIA landmarks
results.total++;
const landmarks = document.querySelectorAll('[role="banner"], [role="main"], [role="complementary"], [role="contentinfo"], header, main, aside, footer');
const uniqueRoles = new Set();
landmarks.forEach(element => {
const role = element.getAttribute('role') || element.tagName.toLowerCase();
uniqueRoles.add(role);
});
const requiredRoles = ['banner', 'main', 'contentinfo'];
let hasRequired = true;
requiredRoles.forEach(role => {
if (!uniqueRoles.has(role)) {
console.error(`❌ Missing ARIA landmark: ${role}`);
hasRequired = false;
}
});
if (hasRequired) {
results.passed++;
} else {
results.failed++;
}
// Test 5: Check focus indicators
results.total++;
const focusableElements = document.querySelectorAll('a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])');
if (focusableElements.length === 0) {
results.passed++;
} else {
// Check if focus styles are defined in CSS
const computedStyle = getComputedStyle(focusableElements[0]);
const outline = computedStyle.outline;
const boxShadow = computedStyle.boxShadow;
if (outline !== 'none' && outline !== '' && outline !== '0px none rgb(0, 0, 0)') {
results.passed++;
} else if (boxShadow && boxShadow !== 'none') {
results.passed++;
} else {
console.warn('⚠️ Focus indicators may not be visible');
results.warnings++;
}
}
// Summary
console.log(`\n📊 Accessibility Test Results:`);
console.log(`✅ Passed: ${results.passed}`);
console.log(`❌ Failed: ${results.failed}`);
console.log(`⚠️ Warnings: ${results.warnings}`);
console.log(`📈 Success Rate: ${Math.round((results.passed / results.total) * 100)}%`);
if (results.failed === 0 && results.warnings === 0) {
console.log('🎉 All accessibility tests passed!');
} else if (results.failed === 0) {
console.log('👍 Accessibility compliant with minor warnings');
} else {
console.log('⚠️ Accessibility issues found - review and fix');
}
return results;
}
File diff suppressed because one or more lines are too long