This post comes from the first version of this blog.
Please send me an email if anything needs an update. Thanks!

Od paru tygodni można w Internecie odwiedzać stronę o dosyć “ciekawym” tytule: “Did you watch porn? See what your friends watch…”, co w wolnym tłumaczeniu brzmi następująco: “Czy oglądałeś dzisiaj pornografię? Zobacz, co oglądali Twoi znajomi…”. Jedyną jej funkcją jest sprawdzenie, czy i na które strony z “odpowiednimi materiałami” wchodził dany internauta. Odpowiedź jest udzielana od razu po wczytaniu strony, oczywiście opcje mamy tylko dwie:

I co z tego?

Prawdopodobnie zadajesz sobie teraz pytanie w stylu: “no i co z tego?” albo “wiele stron pokazuje różne dziwne rzeczy, czym akurat ta miałaby się wyróżniać?” - cóż, masz takie prawo. ;] Aby jednak zachęcić Cię do przeczytania wpisu także pozwolę sobie zadać parę pytań. A więc:

Jeśli chcesz poznać odpowiedzi na te pytania, zapraszam do lektury.

Fabuła.

Link do tej strony internetowej dostałem od koleżanki ze studiów [dzięki Razu!], która w ramach przyjacielskiego żartu chciała sprawdzić, jaki wynik uzyskam w teście. Przy pierwszym wejściu na stronę oczywiście miałem status "Good boy", jakżeby inaczej. ;] Myślę sobie - normalne, w końcu siedzę za firewallem, mam antywirusa, niech ktoś tylko spróbuje coś wziąć z mojego komputera.

W celach naukowych, dla sprawdzenia mojej teorii wszedłem na jedną z popularnych stron o tematyce wszelakiej, będącą wariacją serwisu YouTube z kolorem czerwonym. Jakież było moje zdziwienie, kiedy po ponownym załadowaniu strony DidYouWatchPorn moim oczom ukazał się status “Naughty naughty”, wraz z informacją o adresie “strony testowej”. Od razu napisałem koleżance, że teraz to na pewno nie zostawię tego - muszę dowiedzieć się o co w tym wszystkim chodzi oraz czyj to spisek i zamach na moją prywatność i wolności obywatelskie. ;]

Jak to działa?

Zacząłem studiować kod obu stron ["strony testowej" i DidYouWatchPorn] w poszukiwaniu relacji albo jakichkolwiek żądań AJAXowych, dzięki którym takie dane jak np. adres IP mogłyby zostać uwzględnione w statystykach, niestety bezskutecznie. Pomyślałem po chwili, że to nie ma sensu, w końcu firmy zajmujące się prowadzeniem tego typu stron na pewno nie upubliczniają takich informacji. Skupiłem się więc na samym DidYouWatchPorn, szukając wyjaśnienia.

Ze względu na to, że samym HTMLem nikt jeszcze “świata nie zawojował”, stwierdziłem, że wypadałoby sprawdzić co piszczy w JavaScripcie. W sekcji strony znalazłem kilka plików, z których najbardziej interesujący był css_check.js. Zawiera on kilka funkcji dokonujących sprawdzenia potrzebnych danych. Rozpocząłem analizę od funkcji getVisitedPornSites():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
function getVisitedPornSites() {
	var visitedSited = [];

	for (var i = 0; i < pornSites.length; i++) {
		if (checkLink('http://' + pornSites[i]) || checkLink('http://www.' + pornSites[i])) {
			visitedSited.push(pornSites[i]);
		}
	}

	return visitedSited;
}

Nie ma w niej specjalnie ciekawych rzeczy, ale możemy zauważyć, że każdy wczytany wcześniej adres* jest sprawdzany w pętli funkcją checkLink(). Jeśli przejdzie pomyślnie test, jest dodawany do tablicy. No to jedziemy dalej:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function checkLink(url) {
 if (!checkLink.base) {
 checkLink.base = document.createElement('div');
 checkLink.base.setAttribute("id","tempLinks");
 checkLink.base.style.position = 'absolute';
 checkLink.base.style.width = '1px';
 checkLink.base.style.height = '1px'
 checkLink.base.style.left = '-10px';
 checkLink.base.style.top = '-10px';
 document.body.appendChild(checkLink.base);
 }

 var link = document.createElement('a');
 link.href = url;
 link.innerHTML = '.';
 link.className = 'pornLink';
 checkLink.base.appendChild(link);

 var display = getComputedCssStyle(link, 'display');
 checkLink.base.removeChild(link);

 return display != 'none';
}

Kawałek kodu DOM, nadal nic specjalnego… Produkowany jest element, nadawane są odpowiednie wartości atrybutów. Jak widać wynikiem testu jest to, czy element po przetworzeniu będzie miał wartość atrybutu display inną niż none. Jedyna niestandardowa funkcja to getComputedCssStyle():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
function getComputedCssStyle(element, prop) {
  var d = ownerDocument(element);

  if (element.currentStyle) {
	// IE, Opera
    return element.currentStyle[prop];
  }
  if (d.defaultView && d.defaultView.getComputedStyle) {
	// Gecko,Webkit
	return d.defaultView.getComputedStyle(element, '')[prop] || {};
  }

  return element.style[prop];
}

Wykorzystana jest tutaj własna funkcja ownerDocument() zwracająca element nadrzędny dla danego, następnie mamy instrukcje warunkowe wykrywające funkcjonalności przeglądarek opartych na różnych silnikach [ten sam kod dla IE i Opery?]. Ja skupię się na opisaniu gałęzi dla Gecko i WebKita. Znajduje się w niej całkiem “zwykły” kod, ale w pewnym momencie widzimy wykorzystanie metody getComputedStyle(), którą po sposobie wywołania możemy rozpoznać jako jedną z funkcji DOM. Do czego mogłaby ona służyć?

Magia.

W tym miejscu rozpoczyna się właściwe wyjaśnienie całego mechanizmu. Otóż, praktycznie każda strona internetowa jest mniej lub bardziej "wystylowana" za pomocą arkuszy CSS. Nam potrzebny będzie styl dla odnośników, czyli selektor "a" wraz z pseudoklasą: "a:visited". Ustawiamy odpowiednio oba style, np. tak:
1
2
3
4
5
6
7
8
9
a
 {
 color: #000; // link nieodwiedzony, kolor czarny
 }

a:visited
 {
 color: #F00; // link odwiedzony, kolor czerwony
 }

Za pomocą metody getComputedStyle() możemy pobrać dane renderowania [wszystkie atrybuty CSS wraz z wartościami] danego elementu bez wyświetlania go. Podsumowanie jej działania w serwisie Mozilla Developer Center brzmi następująco:

getComputedStyle() returns the computed style of an element. Computed style represents the final computed values of all CSS properties for the element.
Czyli cała magia polega na tym, że tworzymy nowy obiekt, pobieramy dane jego "wygenerowanego wyglądu" i sprawdzamy, czy pasuje on do stylu pseudoklasy "a:visited". Autorzy strony ułatwili sobie pracę sprawdzając tylko jeden atrybut - display - czy informację o tym, czy dany link się wyświetli. Przy wyświetlaniu linka, jeżeli znajduje się on w historii, zostaną użyte atrybuty i wartości z pseudoklasy odpowiadającej za już odwiedzoną stronę, więc pośrednio możemy stwierdzić, czy dany użytkownik [bardziej dany komputer] już ją odwiedzał.

Czy stanowi to zagrożenie bezpieczeństwa?

Szczerze mówiąc, trudno jednoznacznie stwierdzić, czy jest to jakiś problem dla użytkownika. Informacje o tym, czy dana strona była odwiedzana można potencjalnie wykorzystać w ukierunkowanych na konkretnego klienta [nie, nie użyję słowa target] kampaniach reklamowych i różnych innych statystykach, tak więc osoby przewrażliwione na punkcie prywatności na pewno będą miały za złe twórcom przeglądarek, że udostepniają takie "mroczne" funkcjonalności. Być może kiedyś historia przeglądanych stron internetowych będzie używana jako dowód w sądzie, podobnie jak bilingi telefoniczne, więc może to stanowić problem. Z drugiej strony trudno jest sprawdzić wszystkie możliwe adresy, tak więc myślę, że opisywane przeze mnie możliwości pozostaną [przynajmniej na razie] w sferze "zabawy", tak jak to widać na przykładzie DidYouWatchPorn.

Podsumowanie.

Muszę przyznać, że dawno nie zostałem zaskoczony tak genialnym użyciem [tak na marginesie - angielskie określenie "clever use" podoba mi się strasznie, ale trudno, jak pisać po polsku to konsekwentnie ;]] bardzo prostej w idei i wykonaniu "technologii". Rozpracowanie całego systemu zajęło mi mniej niż pół godziny, ale ilość pochłoniętej wiedzy merytorycznej [tak, należę do ludzi, którzy czytając artykuł na Wikipedii otwierają wszystkie powiązane linki i te powiązane z powiązanymi ;]] była naprawdę imponująca.

Na koniec mała notka dotycząca kodu, który wstawiłem we wpisie [gdyby komuś nie spodobało się to, że wykorzystałem jego fragmenty]:

Licencja / Copyright:

Oświadczam, że wstawiony przeze mnie kod jest autorstwa twórców witryny DidYouWatchPorn znajdującej się pod adresem http://www.didyouwatchporn.com/ i użyłem go jedynie w celu wyjaśnienia mechanizmu działania serwisu. Jeśli zostałoby to uznane za naruszenie prawa autorskiego lub własnościowego, proszę o kontakt, kod zostanie usunięty.

I declare that the code inserted in this blog post belongs to the creators of the DidYouWatchPorn website at http://www.didyouwatchporn.com/ and that I used it in order to explain how the site works only. If this was to be considered a copyright or property violation, please contact me and the code will be removed.