Wstęp#

Skonfigurowałem w front matter przycisk hero z numerem telefonu (url: "tel:+48123123123"). W wyrenderowanym HTML zamiast href="tel:+48123123123" pojawiło się href="#ZgotmplZ". Kliknięcie nie robiło nic, a hover pokazywał zakotwiczenie na bieżącej stronie.

Diagnostyka była nieoczywista, bo hardcodowany link tel: w stopce działał poprawnie. Winowajca leżał na styku dwóch mechanizmów: Hugowego relURL w partialu URL oraz kontekstowego auto-escapowania z pakietu html/template w Go. Poniżej rozbieram, dlaczego #ZgotmplZ w ogóle się pojawia i dlaczego pełna naprawa wymaga zmian jednocześnie w partialu i w miejscach jego wywołania.

Objaw#

Przycisk w sekcji hero skonfigurowany w front matter:

cta_primary:
  text: "Umów wizytę"
  url: "tel:+48123123123"

Przycisk się renderuje, ale po najechaniu widać https://localcraft.com/#ZgotmplZ zamiast tel:+48123123123. Kliknięcie nic nie robi.

Skąd się bierze #ZgotmplZ?#

Pakiet html/template w Go używa kontekstowego auto-escapowania. Kiedy dynamiczna wartość trafia do atrybutu href, silnik szablonów stosuje filtr URL. Jeśli wartość wygląda podejrzanie lub pochodzi z nieoczekiwanego źródła, zostaje zastąpiona ciągiem #ZgotmplZ. To celowy mechanizm: zamiast cicho wprowadzić wektor XSS, strona widocznie się psuje.

Przyczyna: relURL nie rozumie tel:#

Motyw miał partial url.html odpowiedzialny za generowanie poprawnych relatywnych URL-i:

{{- strings.TrimPrefix "/" . | relURL -}}

Funkcja relURL w Hugo jest przeznaczona dla ścieżek takich jak /o-nas/ czy /blog/. Gdy otrzyma tel:+48123123123, nie rozpoznaje tel: jako schematu i traktuje cały string jako ścieżkę względną, produkując coś w stylu /tel:+48123123123. To nie jest prawidłowy URL, więc html/template podmienia go na #ZgotmplZ.

Link z numerem telefonu w stopce był hardcodowany bezpośrednio w szablonie:

<a href="tel:{{ $phoneClean }}">

Tutaj prefiks tel: jest literałem w szablonie, dynamiczny jest tylko numer. Kontekstowy escaper widzi tel: statycznie i przepuszcza bez modyfikacji.

Naprawa#

Krok 1. url.html: pomijamy relURL dla specjalnych schematów#

W partialu URL rozdzielamy dwa przypadki. Dla wartości ze schematami tel:, mailto: czy pełnymi http(s):// zwracamy string bez zmian. Dla ścieżek bez schematu dalej używamy relURL:

{{- if or (hasPrefix . "tel:") (hasPrefix . "mailto:") (hasPrefix . "http://") (hasPrefix . "https://") -}}
  {{- . -}}
{{- else -}}
  {{- strings.TrimPrefix "/" . | relURL -}}
{{- end -}}

Krok 2. Miejsca wywołania: potok przez safeURL#

Partial Hugo zwraca template.HTML. Kiedy ta wartość trafia do atrybutu href, kontekstowy escaper Go nadal stosuje filtr URL. Rozwiązaniem jest potokowanie wyniku partiala przez safeURL w każdym miejscu wywołania:

<a href="{{ partial "url.html" .url | safeURL }}" class="btn btn-primary">

safeURL oznacza wartość jako template.URL, co całkowicie pomija filtr URL. Kontekstowy escaper przechodzi obok, do atrybutu href trafia dokładnie taka wartość, jaką zwrócił partial.

Dlaczego safeURL wewnątrz partiala nie wystarcza?#

Naturalny odruch to dodać safeURL bezpośrednio w partialu, tuż przy wartości zwracanej:

{{- . | safeURL -}}  {{/* to NIE zadziała */}}

To nie wystarczy, bo bufor wyjściowy partiala zbiera wszystko jako template.HTML. Typ template.URL zwrócony przez safeURL gubi się w chwili, gdy partial kończy działanie. Wywołujący szablon widzi wyłącznie string template.HTML i mimo wszystko przepuszcza go przez filtr URL, kończąc znowu na #ZgotmplZ.

Potokowanie safeURL w miejscu wywołania omija ten problem. Wartość trafia bezpośrednio do escapera jako template.URL, filtr URL zostaje pominięty i atrybut href dostaje ciąg dokładnie taki, jaki wypuścił partial.

Zmienione pliki#

PlikZmiana
layouts/partials/url.htmlPomija relURL dla tel:, mailto:, http://, https://
layouts/partials/sections/hero.html| safeURL przy obu linkach CTA
layouts/partials/sections/blog-preview.html| safeURL przy linku CTA
layouts/partials/sections/faq.html| safeURL przy linku CTA
layouts/partials/sections/before-after.html| safeURL przy linku CTA

Podsumowanie#

#ZgotmplZ w atrybucie href to wprost sygnał, że kontekstowy escaper Go uznał wartość za niebezpieczną. Pełna naprawa wymaga pominięcia relURL dla schematów typu tel: czy mailto: w partialu URL oraz użycia safeURL w każdym miejscu wywołania.

Kluczowe obserwacje z tego debuggingu:

  1. #ZgotmplZ oznacza, że html/template w Go zablokował URL uznany za niebezpieczny. To zawsze błąd typu lub kontekstu, nie treści.
  2. relURL służy wyłącznie do ścieżek. Nie przepuszczaj przez niego tel: ani mailto:.
  3. Wyjście partiala Hugo to template.HTML. safeURL wewnątrz partiala jest pochłaniane, dlatego stosuj je w miejscu wywołania.
  4. Hardcodowane prefiksy schematów w szablonach są traktowane jako statyczne i przechodzą filtr URL bez safeURL.

Źródła i dalsze materiały#

  • html/template (Go) – oficjalna dokumentacja pakietu, opis kontekstowego auto-escapowania i wartości ZgotmplZ.
  • Hugo relURL – zakres zastosowania funkcji i jak Hugo traktuje ścieżki względne.
  • Hugo safeURL – jak oznaczyć wartość jako bezpieczny URL i pominąć filtr kontekstowego escapera.