Szkielet dokumentu#

Wstęp#

HTML5 jako kontynuacja dobrze znanego HTML4 będzie miał bardzo wiele punktów wspólnych przy tworzeniu stron. Nie ma sensu ponownie przeklejać opisów. W rozdziale tym zamieszczam krótkie scharakteryzowanie składni języka oraz troszkę podstawowej wiedzy, której zabrakło w poprzednim kursie.

Krótkie wprowadzenie do składni#

Podstawowy dokument HTML5 może wyglądać następująco:

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
<!DOCTYPE html>
<html>
	<head>
		<title>Sample page</title>
	</head>
	<body>
		<h1>Sample page</h1>
		<p>This is a <a href="demo.html">simple</a> sample.</p>
		<!-- this is a comment -->
	</body>
</html>

Jak widać w nowej odsłonie języka mamy bardzo prostą deklarację DOCTYPE. Już nie trzeba pamiętać fikuśnej składni z wersj poprzedniej.

Elementy#

Dokument HTML5 składa się z drzewa elementów oraz tekstu. Każdy element jest oznaczany w źródle przez znacznik otwierający, jak <body>, oraz znacznik zamykający, jak </body>. W niektórych przypadkach znaczniki otwierające i zamykające mogą zostać pominięte - wówczas są regulowane przez inne znaczniki.

Oczywiście i tym razem tagi muszą zostać zagnieżdżone w odpowiedniej kolejności (nie mogą się przeplatać):

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
<p>This is <em>very <strong>wrong</em>!</strong></p>

Prawidłowy zapis:

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
<p>This <em>is <strong>correct</strong>.</em></p>

W specyfikacji dokładnie zdefiniowane wszystkie elementy, które mogą być stosowane w HTML5, oraz reguły ich prawidłowego zagnieżdżania.

Atrybuty#

Elementy mogą mieć atrybuty, które pozwalają kontrolować ich zachowanie (działanie). W poniższym przykładzie mamy odnośnik utworzony za pomocą elementu <a> oraz atrybutu href:

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
<a href="demo.html">simple</a>

Atrybuty umieszczane są wewnątrz znacznika otwierającego, składają się z nazwy oraz wartości oddzielonych znakiem =. Wartość atrybutu można umieścić bez cudzysłowu jeśli nie zawiera znaków spacji oraz ", ', `, =, < lub >. W przeciwnym razie wartość musi zostać otoczona pojedynczym lub podwójnym cudzysłowem. Wartość oraz znak = można całkowicie pominąć jeśli wartością jest pusty łańcuch znaków. Kilka prawidłowych przykładów:

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
<!-- puste atrybuty -->
<input name=address disabled>
<input name=address disabled="">

<!-- atrybuty z wartością -->
<input name=address maxlength=200>
<input name=address maxlength='200'>
<input name=address maxlength="200">

Atrybuty logiczne#

Pewna grupa atrybutów zaliczana jest do grupy atrybutów logicznych (boolean attributes). Obecność samego atrybutu reprezentuje wartość typu prawdziwego (true), a brak tego atrybutu oznacza wartość fałszywą (false).

Jeśli atrybut jest obecny, jego wartością musi być pusty łańcuch znaków lub wartość identyczna z samą nazwą atrybutu (bez początkowych i końcowych białych znaków) - wielkość znaków nie ma znaczenia w przypadku HTML5. Jak wspomniałem w poprzednim punkcie, jeśli wartością jest pusty łańcuch ("" lub '') może on zostać pominięty razem ze znakiem =.

Wartość true i false jest niedozwolona dla atrybutów logicznych. Dla reprezentacji wartości false atrybut należy całkowicie pominąć.

Poniżej prezentuję poprawne przykłady kontrolki typu checkbox z atrybutami logicznymi checked oraz disabled:

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
<label><input type=checkbox checked name=cheese disabled> Cheese</label>

<label><input type=checkbox checked=checked name=cheese disabled=disabled> Cheese</label>

<label><input type='checkbox' checked name=cheese disabled=""> Cheese</label>

Drzewo DOM#

Aplikacje klienckie (np. przeglądarki internetowe) analizują plik z kodem HTML (czyli przeprowadzają parsowanie) przekształcając go w drzewo DOM (Document Object Model). Drzewo DOM jest reprezentacją dokumentu w pamięci.

Drzewo DOM zawiera wiele rodzajów węzłów (nodes), w szczególności węzeł DocumentType, węzeł Text, węzeł Comment, oraz w niektórych przypadkach węzeł ProcessingInstruction.

Nasz pierwszy przykładowy dokument HTML5 zostanie przekształcony do następującej postaci drzewa DOM:

Drzewo DOM dla przykładowego kodu HTML

Rysunek. Drzewo DOM dla przykładowego kodu HTML

Głównym elementem zwanym korzeniem (root) jest html, który zawsze znajduje się na najwyższym szczeblu hierarchii dokumentu HTML5. Zawiera także dwa inne elementy, head oraz body, a także węzeł Text między nimi.

Istnieje spora liczba węzłów Text w drzewie DOM niż można by z początku sądzić. Wynika to z umieszczenia wkodzie źródłowym wielu spacji (reprezentowanych na grafice przez znak "␣") oraz przełamania wiersza ("⏎"). Wszystkie one reprezentowane są w drzewie DOM jako węzyły tekstowe. Jednakże, ze względów historycznych, część z nich zostaje celowo pomijana lub przemieszczana. W szczególności dotyczy to wszystkich białych znaków przed tagiem otwierającym <html> (zostają pominięte) oraz za tagiem zamykającym </body> (zostają przeniesione na koniec zawartości elementu body). Proponuję bardzo dokładnie przeanalizować grafikę z drzewem DOM naszego prostego przykładu.

Element head zawiera element title, który sam w sobie zawiera węzeł Text z treścią "Sample page". Podobnie element body zawiera element h1, element p oraz komentarz.

Tak utworzone drzewo DOM może być przetwarzane za pomocą skryptów umieszczanych na stronie. Skrypty (zazwyczaj JavaScript) to małe programy, które mogą być osadzone za pomocą elementu script lub przy użyciu uchwytów zdarzeń deklarowanych jako wartości atrybutów. Na przykład, w poniższym kodzie mamy formularz zawierający skrypt, który ustawia wyjściową wartość formularza na "Hello World":

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
<form name="main">
	Result: <output name="result"></output>
	<script>
		document.forms.main.elements.result.value = 'Hello World';
	</script>
</form>

Każdy element w drzewie DOM jest reprezentowany przez obiekt. Wszystkie obiekty mają zdefiniowane API, dzięki którym można je modyfikować. Na przykład, odsyłacz (element a w drzewie powyżej) posiada atrybut href, który można zmienić na kilka sposobów:

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
var a = document.links[0]; // wybór pierwszego odsyłacza w dokumencie
a.href = 'sample.html'; // zmiana adresu URL odsyłacza
a.protocol = 'https'; // zmiana pewnej części adresu URL odsyłacza
a.setAttribute('href', 'http://example.com/'); // bezpośrednia zmiana zawartości atrybutu

Ponieważ drzewa DOM są wykorzystywane jako sposób reprezentowania dokumentów HTML5, gdy są przetwarzane i prezentowane przez implementacje (w szczególności przeglądarki internetowe), specyfikacja HTML5 jest w większości sformułowana w kategoriach drzew DOM, zamiast znaczników opisanych powyżej.

Rendering#

Dokumenty HTML5 stanowią opis zawartości interaktywnej treści niezależnej od typu mediów. Dokumenty mogą być wyświetlane na ekranie, przetwarzane przez syntezatory mowy lub obsługiwane przez czytniki braille'a. Aby mieć wpływ na przebieg takiego renderingu, autorzy mogą korzystać z języków stylu, takich jak CSS. W poniższym przykładzie, strona została odpowiednio ostylowana za pomocą CSS:

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
<!DOCTYPE html>
<html>
	<head>
		<title>Sample styled page</title>
	<style>
		body { background: navy; color: yellow; }
	</style>
	</head>
	<body>
		<h1>Sample styled page</h1>
		<p>This page is just a demo.</p>
	</body>
</html>

Tworzenie bezpiecznych aplikacji#

Kiedy HTML5 jest używany do tworzenia interaktywnych stron, należy zachować szczególną ostrożność w kwestii bezpieczeństawa. Trzeba skrupulatnie unikać luk, przez które napastnik może naruszać integralność strony lub użytkowników witryny. Jest to bardzo złożony i trudny do opanowania temat, wykraczający poza ramy specyfikacji HTML5. Pomimo tego, w dokumencie zawarto wprowadzenie do niektórych często spotykanych pułapek w aplikacjach webowych.

Model zabezpieczenia w sieci Web jest oparty na koncepcji pochodzenia (origins) i potencjalnie wiele ataków odbywa się na zasadzie akcji cross-origin. Większość rzeczy, które można zrobić w językach skryptowych po stronie użytkownika dotyczy tego samego źródła (domeny). Jest to sensowne biorąc pod uwagę aspekt bezpieczeństwa, ale czasami irytujące, kiedy chcemy zwiększyć funkcjonalność strony stosując rozwiązania zewnętrznych firm. Istnieje kilka sprawdzonych sposobów rozwiązujących tę kwestię, także najnowsze interfejsy niektórych rozwiązań udostępniają odpowiednie mechanizmy. Należy jednak pamiętać, że każde zewnętrzne źródło (nie będące pod naszą kontrolą) stanowi jednak pewne zagrożenie. Nawet jeśli firmy udostępniające kod są rzetelne, to same mogą stać się ofiarami różnych ataków.

Walidacja danych wejściowych, Cross-site scripting (XSS), SQL injection#

Przyjmując dane z niezaufanego wejscia (untrusted input), przykładowo treści generowane przez użytkowników w postaci komentarzy, wartości w parametrach URL, wiadomości pochodzących z trzecich stron itd., koniecznym jest odpowiednia walidacja danych przed ich użyciem oraz odpowiednie przetworzenie przed wyświetleniem. Niezastosowanie się do tego otwiera drogę do wielu ataków, od potencjalnie łagodnych, takich jak przyjęcie fałszywych danych na temat wieku użytkownika, do bardziej poważnych, jak uruchomienie niebezpiecznych skryptów (kiedy użytkownik przegląda stronę), a nawet katastrofalnych, jak usunięcie wszystkich danych z serwera.

Podczas pisania filtrów do walidacji danych wprowadzanych przez użytkownika, lepiej oprzeć mechanizm filtracyjny na bazie dozwolonych konstrukcji (whitelist-based). Dzięki temu znane złożenia zostaną przepuszczone, a wszystkie pozostałe wykluczone. Tworzenie list blokujących (blacklist-based) wykluczających tylko niektóre zagrożenia nie jest bezpieczne, ponieważ nie wszystko co złe, zostało odkryte i zastosowane do tej pory (i może zostać opracowane w przyszłości).

Oto prosty przykłady strony zwracającej dane, które zostają przekazane w tzw. query string (metoda GET) a następnie wyświetlone w pliku odpowiedzi:

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
<ul>
	<li><a href="message.cgi?say=Hello">Say Hello</a>
	<li><a href="message.cgi?say=Welcome">Say Welcome</a>
	<li><a href="message.cgi?say=Kittens">Say Kittens</a>
</ul>

Bez zaimplementowania odpowiedniej walidacji danych, osoba atakująca może przemycić do systemu niebezpieczny skrypt, wystarczy odpowiednio zmanipulować adres URL:

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
http://example.com/message.cgi?say=%3Cscript%3Ealert%28%27Oh%20no%21%27%29%3C/script%3E

Jeśli atakujący przekona użytkownika do odwiedzenia tak zmanipulowanego adresu, skrypt zostanie uruchomiony na stronie. Taki skrypt może zrobić dowolną liczbę wrogich akcji, ograniczonych tylko przez to, co witryna sama oferuje: jeśli strona jest sklepem e-commerce, wrogi skrypt może np. wykonać wiele niechcianych zakupów. Jest to tak zwany atak cross-site scripting (XSS).

Istnieje znacznie więcej sposobów pozwalających oszukać stronę i umożliwić wykonanie niebezpiecznego kodu. Oto kilka z nich, które powinny być brane pod uwagę przy tworzeniu filtrów typu whitelist-based:

Cross-site request forgery (CSRF)#

Jeśli strona pozwala użytkownikowi na wysyłanie formularzy, np. umieszczania wiadomości na forum pod nazwą użytkownika, dokonywania zakupów, albo ubiegających się o paszport, ważna jest weryfikacja przeprowadzenia akcji, czy wykonana została przez użytkownika celowo, a nie na innej stronie oszukującej użytkownika i wykonującej zapytanie bez jego wiedzy.

Ten problem występuje ponieważ formularze można przesyłać do zewnętrznych źródeł (znajdujących się poza naszą domeną).

Strona może zapobiec takim atakom poprzez wypełnianie formularzy ukrytymi znakami, albo poprzez sprawdzanie nagłówków pochodzenia (origin headers) dla wszystkich żądań.

Clickjacking#

Strona zapewniająca użytkownikom interfejs do wykonania pewnych czynności musi go tak zaimplementować, aby uchronić użytkownika przed różnymi trikami aktywującymi ten interfejs.

Jednym ze sposobów oszukujących użytkownika jest umieszczenie przez niebezpieczną stronę małej ramki iframe a następnie przekonania go do kliknięcia, na przykład w odpowiedzi na rekację w czasie gry. Kiedy użytkownik prowadzi rozgrywkę, niebezpieczna strona może szybko wypozycjonować element iframe pod kursorem myszki, tam gdzie użytkownik zamierza kliknać, przez co aktywuje on inny interfejs.

Aby tego uniknąć, strony które nie będą stosowane w ramkach powinny udostępniać swój interfejs tylko po wykryciu, że nie znajdują się w żadnej ramce (np. poprzez porównianie obiektu window z wartością atrybutu top).

Unikanie pułapek przy użyciu API skryptowego#

Skrypty w HTML5 mają semantykę określaną jako "run-to-completion", co oznacza, że przeglądarka będzie generalnie wykonywać skrypt nieprzerwanie zanim wykona cokolwiek innego, np. odpalenie kolejnego zdarzenia albo kontynuowanie parsowania dokumentu.

Z drugiej strony, parsowanie plików HTML5 odbywa się asynchronicznie i stopniowo, co oznacza, że parser może wstrzymać pracę w dowolnym miejscu i pozwolić skryptowi na uruchomienie. Na ogół jest to dobra rzecz, ale autorzy muszą uważać, aby nie umieszczać uchwytów dla zdarzeń już po odpalonych zdarzeniach.

Istnieją dwie techniki rozwiązujące problem: za pomocą uchwytów zdarzeń w treści atrybutów, lub poprzez utworzenie elementu i dodanie uchwytów w tym samym skrypcie. Druga metoda jest bezpieczniejsza, ponieważ jak wspomniałem wcześniej, skrypty działają nieprzerwanie, zanim kolejne zdarzenie może wystąpić.

Problem może być szczególnie widoczny w przypadku elementu img oraz zdarzenia load. Zdarzenie może zostać odpalone tak szybko jak tylko element zostanie przeanalizowany, zwłaszcza jeśli obraz jest już buforowany (zazwyczaj tak jest).

Autor może w następujący sposób zastosować uchwyt onload na elemencie img aby przechwycić zdarzenie load:

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
<img src="games.png" alt="Games" onload="gamesLogoHasLoaded(event)">

Jeśli element jest dodawany przez skrypt, wówczas tak długo jak uchwyty zdarzeń są dodawane w tym samym skrypcie, żadne zdarzenie nie może zostać pominięte:

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
<script>
	var img = new Image();
	img.src = 'games.png';
	img.alt = 'Games';
	img.onload = gamesLogoHasLoaded;
	// img.addEventListener('load', gamesLogoHasLoaded, false); // alternatywny sposób
</script>

Jednakże, jeśli autor najpierw utworzy element img a następnie w osobnym skrypcie doda uchwyty zdarzeń, istnieje szansa, że zdarzenie load zostanie odpalone pomiędzy tymi akcjami, a w konsekwencji zostanie przegapione:

  1. L
  2. K
  3. T'
  4. T
  5. A
  6. O
  7. Z'
  8. Z
  9. #
<!-- Nie używaj tego stylu, to może powodować sytuację wyścigu! -->

<img id="games" src="games.png" alt="Games">
<!-- Zdarzenie 'load' może zostać odpalone w tym miejscu,
	kiedy parser wstrzymuje pracę, w tym przypadku tego nie dostrzeżemy! -->
<script>
	var img = document.getElementById('games');
	img.onload = gamesLogoHasLoaded; // może nigdy nie zostać odpalone!
</script>
Pasek społecznościowy

SPIS TREŚCI AKTUALNEJ STRONY

Szkielet dokumentu (H1) Wstęp (H2) Krótkie wprowadzenie do składni (H3) Elementy (H4) Atrybuty (H4) Atrybuty logiczne (H4) Drzewo DOM (H4) Rendering (H4) Tworzenie bezpiecznych aplikacji (H3) Walidacja danych wejściowych, Cross-site scripting (XSS), SQL injection (H4) Cross-site request forgery (CSRF) (H4) Clickjacking (H4) Unikanie pułapek przy użyciu API skryptowego (H3)