Commit c3f2ae53 authored by Claude's avatar Claude

fix(website): guard editor-supplied URLs against unsafe schemes

Menu items, button links, announcement bars, popups, floating buttons and
navbar CTAs all wrote editor-supplied values straight into href attributes.
Any user with settings.manage could store a `javascript:` URL and have it run
for every visitor of that tenant's public site.

Adds safe_url(), which allow-lists http/https/mailto/tel/whatsapp, site-relative
paths and fragments, and rejects protocol-relative URLs, data:, vbscript: and
entity/whitespace/control-character obfuscation before testing the scheme.

Applied at four layers so no writer can bypass it:
  - input     MenuManager rules + BlockField Link validation
  - model     WebsiteMenuItem::href()
  - render    every editor-supplied href in every website view
  - import    WebsiteBlueprintService, since blueprint files skip form rules

safe_url() returns null rather than '#', so blocks skip the link entirely
instead of emitting a dead anchor — this satisfies the project's no-href="#"
rule with the same mechanism.

Also fixes Alpine expression injection in the gallery and schedule lightboxes,
where a quote inside an image URL could break out of the inline handler: Blade
escapes ' to ' but the browser decodes it before Alpine parses.
Co-Authored-By: 's avatarClaude Opus 5 <noreply@anthropic.com>
parent 39f468a9
......@@ -35,7 +35,7 @@
@if(!empty($block['links']))
@foreach($block['links'] as $link)
<li>
<a href="{{ $link['url'] ?? '#' }}" class="{{ $mutedColor }} hover:text-[var(--site-accent)] transition-colors text-sm">
<a href="{{ safe_url($link['url'] ?? null) ?: url('/') }}" class="{{ $mutedColor }} hover:text-[var(--site-accent)] transition-colors text-sm">
{{ $link['label'] ?? '' }}
</a>
</li>
......@@ -86,7 +86,7 @@
<div class="flex items-center gap-3 flex-wrap">
@foreach(['facebook', 'instagram', 'twitter', 'youtube', 'tiktok'] as $platform)
@if(!empty($settings->social_links[$platform]))
<a href="{{ $settings->social_links[$platform] }}" target="_blank" rel="noopener"
<a href="{{ safe_url($settings->social_links[$platform] ?? null) ?: url('/') }}" target="_blank" rel="noopener"
class="w-10 h-10 rounded-lg bg-white/5 border {{ $borderColor }} flex items-center justify-center {{ $mutedColor }} hover:text-[var(--site-accent)] hover:border-[var(--site-accent)] transition-all"
aria-label="{{ $platform }}">
@include("website.icons.{$platform}")
......@@ -130,7 +130,7 @@ class="w-10 h-10 rounded-lg bg-white/5 border {{ $borderColor }} flex items-cent
<div class="flex items-center gap-3 mt-6">
@foreach(['facebook', 'instagram', 'twitter', 'youtube', 'tiktok'] as $platform)
@if(!empty($settings->social_links[$platform]))
<a href="{{ $settings->social_links[$platform] }}" target="_blank" rel="noopener"
<a href="{{ safe_url($settings->social_links[$platform] ?? null) ?: url('/') }}" target="_blank" rel="noopener"
class="w-10 h-10 rounded-lg bg-white/5 border {{ $borderColor }} flex items-center justify-center {{ $mutedColor }} hover:text-[var(--site-accent)] hover:border-[var(--site-accent)] transition-all"
aria-label="{{ $platform }}">
@include("website.icons.{$platform}")
......
......@@ -26,7 +26,7 @@
{{ $navSection->title ?? $navSection->section_key->label() }}
</a>
@endforeach
<a href="{{ $settings->navbar_cta_link ?? ($navBase . '#section-contact') }}" class="ms-2 site-btn site-btn--primary text-sm !py-1.5 !px-4">
<a href="{{ safe_url($settings->navbar_cta_link ?? null) ?: ($navBase . '#section-contact') }}" class="ms-2 site-btn site-btn--primary text-sm !py-1.5 !px-4">
{{ $settings->navbar_cta_text ?? 'سجّل الآن' }}
</a>
</div>
......
......@@ -18,7 +18,7 @@
</div>
<div class="flex items-center gap-2">
<a href="{{ $settings->navbar_cta_link ?? ($navBase . '#section-contact') }}" class="hidden sm:inline-flex site-btn site-btn--primary text-sm !py-1.5 !px-4 !rounded-full">
<a href="{{ safe_url($settings->navbar_cta_link ?? null) ?: ($navBase . '#section-contact') }}" class="hidden sm:inline-flex site-btn site-btn--primary text-sm !py-1.5 !px-4 !rounded-full">
{{ $settings->navbar_cta_text ?? 'سجّل الآن' }}
</a>
<button type="button" class="lg:hidden p-2 rounded-lg text-white/80 hover:text-white hover:bg-white/10 transition-colors" onclick="document.getElementById('mobile-menu').classList.toggle('hidden')" aria-label="القائمة">
......
......@@ -15,7 +15,7 @@
{{ __('لوحة التحكم') }}
</a>
@endauth
<a href="{{ $settings->navbar_cta_link ?? ($navBase . '#section-contact') }}" class="site-btn site-btn--primary text-sm !py-2 !px-5">
<a href="{{ safe_url($settings->navbar_cta_link ?? null) ?: ($navBase . '#section-contact') }}" class="site-btn site-btn--primary text-sm !py-2 !px-5">
{{ $settings->navbar_cta_text ?? 'سجّل الآن' }}
</a>
<button type="button" class="lg:hidden p-2 rounded-lg text-white/80 hover:text-white hover:bg-white/10 transition-colors" onclick="document.getElementById('mobile-menu').classList.toggle('hidden')" aria-label="القائمة">
......
......@@ -18,7 +18,7 @@
</div>
<div class="flex items-center gap-3">
<a href="{{ $settings->navbar_cta_link ?? ($navBase . '#section-contact') }}" class="hidden sm:inline-flex site-btn site-btn--primary text-sm !py-2 !px-4">
<a href="{{ safe_url($settings->navbar_cta_link ?? null) ?: ($navBase . '#section-contact') }}" class="hidden sm:inline-flex site-btn site-btn--primary text-sm !py-2 !px-4">
{{ $settings->navbar_cta_text ?? 'سجّل الآن' }}
</a>
@auth
......
......@@ -18,7 +18,7 @@
</div>
<div class="flex items-center gap-3">
<a href="{{ $settings->navbar_cta_link ?? ($navBase . '#section-contact') }}" class="hidden sm:inline-flex site-btn site-btn--primary text-sm !py-2 !px-4">
<a href="{{ safe_url($settings->navbar_cta_link ?? null) ?: ($navBase . '#section-contact') }}" class="hidden sm:inline-flex site-btn site-btn--primary text-sm !py-2 !px-4">
{{ $settings->navbar_cta_text ?? 'سجّل الآن' }}
</a>
@auth
......
......@@ -15,7 +15,7 @@ class="relative overflow-hidden"
style="background-color: {{ $bar['bg_color'] ?? 'var(--site-accent)' }}; color: {{ $bar['text_color'] ?? '#ffffff' }}">
<div class="max-w-7xl mx-auto px-4 py-2.5 flex items-center justify-center gap-3 text-sm font-medium">
@if(!empty($bar['link']))
<a href="{{ $bar['link'] }}" class="hover:underline">
<a href="{{ safe_url($bar['link'] ?? null) ?: url('/') }}" class="hover:underline">
{{ $bar['text'] ?? '' }}
</a>
@else
......
......@@ -26,7 +26,7 @@ class="w-11 h-11 rounded-full bg-gray-800/80 text-white shadow-lg hover:bg-gray-
{{-- Custom Button --}}
@if(($customBtn['enabled'] ?? false) && !empty($customBtn['url']))
<a href="{{ $customBtn['url'] }}"
<a href="{{ safe_url($customBtn['url'] ?? null) ?: url('/') }}"
class="px-4 py-2.5 rounded-full text-white text-sm font-medium shadow-lg hover:shadow-xl transition-all hover:scale-105 flex items-center gap-2"
style="background-color: {{ $customBtn['color'] ?? 'var(--site-accent)' }}">
@if(!empty($customBtn['icon']) && $customBtn['icon'] === 'phone')
......
......@@ -66,7 +66,7 @@ class="absolute top-3 end-3 z-10 w-8 h-8 rounded-full bg-black/10 hover:bg-black
<p class="mt-3 text-gray-600 leading-relaxed text-sm">{{ $popup['body'] }}</p>
@endif
@if(!empty($popup['cta_text']) && !empty($popup['cta_url']))
<a href="{{ $popup['cta_url'] }}"
<a href="{{ safe_url($popup['cta_url'] ?? null) ?: url('/') }}"
class="mt-5 inline-block px-6 py-2.5 rounded-full text-white text-sm font-medium shadow-lg hover:shadow-xl transition-all hover:scale-105"
style="background-color: {{ $popup['cta_color'] ?? 'var(--site-accent)' }}">
{{ $popup['cta_text'] }}
......
......@@ -181,7 +181,7 @@ class="w-full px-4 py-3 bg-white border border-gray-200 rounded-xl text-gray-900
<div class="flex items-center gap-3">
@foreach(['facebook', 'instagram', 'twitter', 'youtube', 'tiktok'] as $platform)
@if(!empty($settings->social_links[$platform]))
<a href="{{ $settings->social_links[$platform] }}"
<a href="{{ safe_url($settings->social_links[$platform] ?? null) ?: url('/') }}"
target="_blank"
rel="noopener"
class="w-10 h-10 rounded-lg bg-gray-50 border border-gray-200 flex items-center justify-center text-gray-400 hover:text-[var(--site-accent)] hover:border-[var(--site-accent)] transition-all">
......
......@@ -11,7 +11,7 @@
@endif
<div class="mt-10 flex flex-col sm:flex-row items-center justify-center gap-4 reveal reveal--delay-2">
<a href="{{ $section->getSetting('cta_link', '#section-contact') }}"
<a href="{{ safe_url($section->getSetting('cta_link')) ?: '#section-contact' }}"
class="site-btn bg-white text-gray-900 font-bold hover:bg-white/90 hover:scale-105 transition-all shadow-xl">
{{ $section->getSetting('cta_text', 'سجّل الآن') }}
<svg class="w-5 h-5 rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 8l4 4m0 0l-4 4m4-4H3"/></svg>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment