security: fix 27 vulnerabilities + UI integration (Issues tab, project nav, validation)
Security fixes (27 vulnerabilities across 20 files): CRITICAL: - MediaManager: whitelist mediable types prevents RCE via class instantiation - MediaManager/OfflineSyncController: IDOR fixes, remove Auth::id()??1 fallback - ClientProjects: verify project ownership on all mutations (IDOR) - CompanyManagement: Admin role check on mount() and mutations (auth bypass) - ProjectMap: scope feature/template lookups to current project (IDOR x5) - PhaseList/TemplateManager/LayerManager: scope mutations to owned resources (IDOR) - ProjectEditTabs: Gate::authorize on mount() and updateProject() - routes/web.php: reports routes moved inside can:manage all middleware (auth bypass) MEDIUM: - layer-manager: escapeHtml() on Leaflet popup interpolations (XSS) - MediaManager: server-side MIME validation + 50MB limit - ProjectList/ProjectUsers/ProjectCompanies/PhaseProgress: auth checks added - AdminUsers/ReportsDashboard/ExportController: role/permission checks added LOW: - config/session.php: secure cookie tied to production env - OfflineSyncController: sanitize storage path (path traversal) UI integration: - project-map: Issues tab (4th) with open-count badge - project-map: project navigation bar (Dashboard/Map/Gantt/Report/Issues) - project-dashboard: action buttons for Map/Gantt/Report/Issues - project-form: validation error summary + per-field @error spans - template-manager: validation error display Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -22,7 +22,7 @@
|
||||
<input type="color" wire:model="layerColor" class="input input-bordered">
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label">{{ __('File') }} (GeoJSON, KML, Shapefile.zip, DWG)</label></br>
|
||||
<label class="label">Archivo (GeoJSON, KML, Shapefile.zip, DWG)</label></br>
|
||||
<input type="file" wire:model="uploadFile" class="file-input file-input-bordered">
|
||||
@error('uploadFile') <span class="text-error">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
@@ -49,13 +49,13 @@
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<button wire:click="selectLayer({{ $layer->id }})" class="btn btn-xs btn-info">✏️ {{ __('Edit') }}</button>
|
||||
<button wire:click="deleteLayer({{ $layer->id }})" wire:confirm="{{ __('Delete layer') }}?" class="btn btn-xs btn-error">🗑️</button>
|
||||
<button wire:click="selectLayer({{ $layer->id }})" class="btn btn-xs btn-info">✏️ Editar</button>
|
||||
<button wire:click="deleteLayer({{ $layer->id }})" class="btn btn-xs btn-error" onclick="return confirm('¿{{ __("Delete layer") }}?')">🗑️</button>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
@if($layers->isEmpty())
|
||||
<p class="text-center">{{ __("No layers. Create or import one.") }}</p>
|
||||
<p class="text-center">{{ __("No results") }}. Crea una o importa.</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@@ -69,8 +69,8 @@
|
||||
<h2 class="card-title">{{ __("Edit") }}</h2>
|
||||
@if($selectedLayer)
|
||||
<div class="mt-3 flex gap-2">
|
||||
<button id="saveDrawingBtn" class="btn btn-primary">💾 {{ __('Save changes') }}</button>
|
||||
<button wire:click="cancelEditing" class="btn btn-outline">{{ __('Cancel') }}</button>
|
||||
<button id="saveDrawingBtn" class="btn btn-primary">💾 Guardar cambios</button>
|
||||
<button wire:click="cancelEditing" class="btn btn-outline">Cancelar edición</button>
|
||||
</div>
|
||||
@endif
|
||||
<div id="permanentMap" style="flex: 1; min-height: 500px; width: 100%; background: #e2e8f0;" wire:ignore></div>
|
||||
@@ -88,6 +88,17 @@
|
||||
let allLayersData = {}; // id -> {geojson, color}
|
||||
let visibleLayerIds = [];
|
||||
|
||||
// XSS-safe HTML escaping for user-supplied data rendered in Leaflet popups
|
||||
function escapeHtml(text) {
|
||||
if (text === null || text === undefined) return '';
|
||||
return String(text)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
// Inicialización del mapa
|
||||
function initMap() {
|
||||
if (map) return;
|
||||
@@ -137,9 +148,9 @@
|
||||
style: { color: data.color, weight: 3, opacity: 0.8, fillOpacity: 0.3 },
|
||||
onEachFeature: (feature, layer) => {
|
||||
const props = feature.properties;
|
||||
const content = `<b>${props.name || 'Elemento'}</b><br>
|
||||
Progreso: ${props.progress || 0}%<br>
|
||||
Responsable: ${props.responsible || '-'}`;
|
||||
const content = `<b>${escapeHtml(props.name) || 'Elemento'}</b><br>
|
||||
Progreso: ${escapeHtml(props.progress) || 0}%<br>
|
||||
Responsable: ${escapeHtml(props.responsible) || '-'}`;
|
||||
layer.bindPopup(content);
|
||||
}
|
||||
}).addTo(displayGroup);
|
||||
@@ -158,10 +169,10 @@
|
||||
onEachFeature: (f, l) => {
|
||||
l.feature = f;
|
||||
const props = f.properties;
|
||||
const content = `<b>${props.name || @js(__('Feature'))}</b><br>
|
||||
@js(__('Progress')): ${props.progress || 0}%<br>
|
||||
@js(__('Responsible')): ${props.responsible || '-'}<br>
|
||||
<em>@js(__('Editable'))</em>`;
|
||||
const content = `<b>${escapeHtml(props.name) || 'Elemento'}</b><br>
|
||||
Progreso: ${escapeHtml(props.progress) || 0}%<br>
|
||||
Responsable: ${escapeHtml(props.responsible) || '-'}<br>
|
||||
<em>Editable</em>`;
|
||||
l.bindPopup(content);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user