├── .htaccess ├── LICENSE ├── README.md ├── config.php ├── etag.diff ├── etags.jpg ├── fingerprinting.jpg ├── index.php └── sessions └── index.php /.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine On 2 | RewriteRule ^tracker.jpg$ index.php?tracker -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2004 Sam Hocevar 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Cookieless cookies 2 | ================= 3 | 4 | This repository is a demonstration of tracking users by using etags instead of cookies (or localstorage or anything else). 5 | 6 | http://lucb1e.com/rp/cookielesscookies 7 | 8 | Contact 9 | ------- 10 | 11 | Please don't use the email address included in the commits, I am receiving spam on there so I'll block that address. Use https://lucb1e.com/email-address instead or open a ticket here on GitHub (I'll get an email notification of that as well), thanks! 12 | 13 | License 14 | ------- 15 | 16 | Copyright © 2013 lucb1e. 17 | 18 | This work is free. You can redistribute it and/or modify it under the 19 | terms of the Do What The Fuck You Want To Public License, Version 2, 20 | as published by Sam Hocevar. See the `LICENSE` file for more details. 21 | -------------------------------------------------------------------------------- /config.php: -------------------------------------------------------------------------------- 1 | $etag = substr(sha1($secret . sha1($_SERVER["REMOTE_ADDR"]) . sha1($_SERVER["HTTP_USER_AGENT"])), 0, 18); 9 | 60,85c57,59 10 | < header("Content-type: image/png"); 11 | < 12 | < $im = imagecreatetruecolor(400, 60); 13 | < 14 | < // Create some colors 15 | < $white = imagecolorallocate($im, 255, 255, 255); 16 | < $grey = imagecolorallocate($im, 128, 128, 128); 17 | < $black = imagecolorallocate($im, 0, 0, 0); 18 | < imagefilledrectangle($im, 0, 0, 399, 59, $white); 19 | < 20 | < // The text to draw 21 | < $text = 'visitor id: ' . $etag; 22 | < // Replace path by your own font path 23 | < $font = './font.ttf'; 24 | < 25 | < // Add some shadow to the text 26 | < //imagettftext($im, 15, 0, 11, 21, $grey, $font, $text); 27 | < // Add the text 28 | < imagettftext($im, 15, 0, 10, 20, $black, $font, $text); 29 | < $text = "number visits: " . $session["visits"]; 30 | < imagettftext($im, 15, 0, 10, 50, $black, $font, $text); 31 | < 32 | < // Using imagepng() results in clearer text compared with imagejpeg() 33 | < imagepng($im); 34 | < imagedestroy($im); 35 | < 36 | --- 37 | > header("Content-type: image/jpeg"); 38 | > header("Content-length: " . filesize("fingerprinting.jpg")); 39 | > readfile("fingerprinting.jpg"); 40 | -------------------------------------------------------------------------------- /etags.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucb1e/cookielesscookies/20d08d82f3678986f8e493671af39fef65a92673/etags.jpg -------------------------------------------------------------------------------- /fingerprinting.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucb1e/cookielesscookies/20d08d82f3678986f8e493671af39fef65a92673/fingerprinting.jpg -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | github.com/lucb1e/cookielesscookies"); 4 | 5 | require("config.php"); // for $secret. 6 | 7 | $sessionsdir = "sessions/"; 8 | 9 | // An ETag was sent to the webserver 10 | if (!empty($_SERVER["HTTP_IF_NONE_MATCH"])) { 11 | // This is what you would normally do 12 | $etag = substr(str_replace(".", "", str_replace("/", "", str_replace("\\", "", $_SERVER["HTTP_IF_NONE_MATCH"]))), 0, 18); 13 | } 14 | else { // No etag was sent. We need to generate one. Normally you would derive this from randomness. 15 | $etag = substr(sha1($secret . sha1($_SERVER["REMOTE_ADDR"]) . sha1($_SERVER["HTTP_USER_AGENT"])), 0, 18); 16 | } 17 | 18 | // Initialize a new or existing session given any etag. 19 | function initsession($etag, $force_reinit = false) { 20 | global $session, $sessionsdir; 21 | if (!$force_reinit && file_exists($sessionsdir . $etag)) { 22 | $session = unserialize(file_get_contents($sessionsdir . $etag)); 23 | } 24 | else { 25 | $session = array("visits" => 1, "last_visit" => time(), "your_string" => ""); 26 | } 27 | } 28 | 29 | function updatesession() { 30 | global $session; 31 | $session["visits"] += 1; 32 | $session["last_visit"] = time(); 33 | } 34 | 35 | // Write any changes to the disk 36 | function storesession($etag) { 37 | global $session, $sessionsdir; 38 | $fid = fopen($sessionsdir . $etag, "w"); 39 | fwrite($fid, serialize($session)); 40 | fclose($fid); 41 | } 42 | 43 | initsession($etag); 44 | 45 | // .htaccess rewrites to ?tracker if the 'tracker.jpg' file is requested. 46 | if (isset($_GET["tracker"])) { 47 | // No ETag sent? Make sure we use a new session. 48 | if (empty($_SERVER["HTTP_IF_NONE_MATCH"])) { 49 | @unlink($sessionsdir . $etag); // may or may not exist 50 | unset($session); 51 | initsession($etag); 52 | } 53 | updatesession(); 54 | storesession($etag); 55 | header("Cache-Control: private, must-revalidate, proxy-revalidate"); 56 | header("ETag: " . substr($etag, 0, 18)); // our "cookie" 57 | header("Content-type: image/jpeg"); 58 | header("Content-length: " . filesize("fingerprinting.jpg")); 59 | readfile("fingerprinting.jpg"); 60 | exit; 61 | } 62 | 63 | // Vulnerable to CSRF attacks, I know. I didn't think it really mattered 64 | // since XSS is impossible and no important data is stored. 65 | if (isset($_POST["newstring"])) { 66 | $session["your_string"] = substr(htmlentities($_POST["newstring"]), 0, 500); 67 | storesession($etag); 68 | header("Location: ./"); 69 | exit; 70 | } 71 | 72 | ?> 73 | 74 | 75 | 76 | Lucb1e.com :: Cookieless Cookies 77 | 82 | 83 | 84 |
85 |
86 | 87 |
88 |

Cookieless cookies

89 | 90 | There is another obscure way of tracking users without using cookies or even Javascript. It has 91 | already been used 92 | by numerous websites but few people know of it. This page explains how it works and how to protect yourself.
93 |
94 |
95 | This tracking method works without needing to use:
96 | 104 | 105 | Instead, it uses another type of storage that is persistent between browser restarts: caching.
106 |
107 | Even when you disabled cookies entirely, have Javascript turned off and use a VPN service, this technique will 108 | still be able to track you.
109 |
110 |
111 | 112 | 113 |

Demonstration

114 | Update (2025): Someone else made a better version here: potatocrunchcereal.com/cookielesscookies/
115 |
116 | As you read this, you have already been tagged. Sorry. The good news is that I don't link your session identifier to any 117 | personal information. Here is everything I store about you right now:
118 |
119 |
120 | Number of visits:
121 |
122 | Last visit:
123 |
124 | Want to store some text here?
125 |
126 | (max. 350 characters)
127 | 128 |
129 |
130 | Go ahead, type something and store it. Then close your browser and open this page again. Is it still there?
131 |
132 | Check your cookies, is anything there? Nope, it's all in a fake image checksum that almost noone is aware of. 133 | Saw that eye on the right top of the page? That's our tracker.
134 |
135 |
136 | 137 | 138 |

So how does this work?

139 | This is a general overview:
140 |
141 |
142 |
143 | The ETag shown in the image is a sort of checksum. When the image changes, the checksum changes. So when the browser 144 | has the image and knows the checksum, it can send it to the webserver for verification. The webserver then checks 145 | whether the image has changed. If it hasn't, the image does not need to be retransmitted and lots of data is saved.
146 |
147 | Attentive readers might have noticed already how you can use this to track people: the browser sends the information back 148 | to the server that it previously received (the ETag). That sounds an awful lot like cookies, doesn't it? The server can 149 | simply give each browser an unique ETag, and when they connect again it can look it up in its database.
150 |
151 | 152 | Technical stuff (and bugs) specifically about this demo
153 | Update (2025): Note that the better version linked above should not have these problems.
154 |
155 | To demonstrate how this works without having to use Javascript, I had to find a piece of information that's relatively 156 | unique to you besides this ETag. The image is loaded after the page is loaded, but only the image contains the 157 | ETag. How can I display up to date info on the page? Turns out I can't really do that without dynamically updating the 158 | page, which requires javascript, which I wanted to avoid to show that it can be done without.
159 |
160 | This chicken and egg problem introduces a few bugs:
161 | - All information you see was from your previous pageload. Press F5 to see updated data.
162 | - When you visit a page where you don't have an ETag (like incognito mode), your session will be emptied. Again, this 163 | is only visible when you reload the page.
164 |
165 | I did not see a simple solution to these issues. Sure some things can be done, but nothing that other websites would use, 166 | and I wanted to keep the code as simple and as close to reality as possible.
167 |
168 | Note that these bugs normally don't exist when you really want to track someone because then you don't intend to show users 169 | that they are being tracked.
170 |
171 | Source code
172 | What's a project without source code? Oh right, Microsoft Windows.
173 |
174 | https://github.com/lucb1e/cookielesscookies
175 |
176 |
177 | 178 | 179 |

What can we do to stop it?

180 | One thing I would strongly recommend you to do anytime you visit a page where you want a little more 181 | security, is opening a private navigation window and using https exclusively. Doing this single-handedly 182 | eliminates attacks like BREACH (the latest https hack), disables any and all tracking cookies that you 183 | might have, and also eliminates cache tracking issues like I'm demonstrating on this page. I use this 184 | private navigation mode when I do online banking. In Firefox (and I think MSIE too) it's Ctrl+Shift+P, 185 | in Chrome it's Ctrl+Shift+N.
186 |
187 | Besides that, it depends on your level of paranoia.
188 |
189 | I currently have no straightforward answer since cache tracking is virtually undetectable, but also because caching 190 | itself is useful and saves people (including you) time and money. Website admins will consume less bandwidth (and 191 | if you think about it, in the end users are the ones that will have to pay the bill), your pages will load faster, 192 | and especially on mobile devices it makes a big difference if you don't have an unlimited 4G plan. It's even 193 | worse when you have a high-latency or low-bandwidth connection because you live in a rural area.
194 |
195 | If you're very paranoid, it's best to just disable caching altogether. This will stop any such tracking from 196 | happening, but I personally don't believe it's worth the downsides.
197 |
198 | The Firefox add-on Self-Destructing Cookies has the ability to empty your cache when you're not using your 199 | browser for a while. This might be an okay alternative to disabling caching; you can only be tracked during 200 | your visit, and they can already do that anyway by following which pages were visited by which IP address, so 201 | that's no big deal. Any later visits will appear as from a different user, assuming all other tracking 202 | methods have already been prevented.
203 |
204 | I'm not aware of any add-on that periodically removes your cache (e.g. once per 72 hours), but there might be. 205 | This would be another good alternative for 99% of the users because it has a relatively low performance impact 206 | while still limiting the tracking capabilities.
207 |
208 | Update (2013): I've heard the Firefox add-on SecretAgent also does ETag overwriting to prevent this kind of tracking 209 | method. You can whitelist websites to re-enable caching there while blocking tracking by other domains. It has been 210 | confirmed that this 211 | add-on stops the tracking. SecretAgent's website.
212 |
213 |
214 | Update (2020): A reader made a tool similar to EFF's Panopticon. It tries to check a few more things, and while it 215 | bugged out for me in Firefox, it's another interesting approach that might give you more insight if it works for you.
216 | https://privacy.net/analyzer/
217 |
218 |
219 | Update (2025): Another reader made an improved version, I added the link in the text above. 220 | Their page does not have a source code link, but a diff was shared with me under the same license: etag.diff. 221 | Thanks! 222 |
223 |
224 | Liked this? Follow me on Twitter or Google Plus! 225 | So as of, ehm, a while, I'm not on Twitter anymore and RIP G+. To get in touch, use lucb1e.com/email-address. 226 | I'm on Mastodon but under a different name, feel free to ask it by email 227 | 228 |
229 | Written by lucb1e in 2013.
230 | All text, resources and methods on this page are hereby released as WTFPL - www.wtfpl.net 231 |
232 |
233 |
 
234 | 235 | 236 | -------------------------------------------------------------------------------- /sessions/index.php: -------------------------------------------------------------------------------- 1 | No directory listing for obvious reasons. --------------------------------------------------------------------------------