├── main.css ├── LICENSE ├── track.php ├── results.php ├── index.html ├── README.zh.md ├── README.md └── screen.css /main.css: -------------------------------------------------------------------------------- 1 | @media screen { 2 | @import url("screen.css"); 3 | } 4 | 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Jan Böhmer 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /track.php: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |

Results

20 | When the value is empty or PHP notices appears, this mean that the value is false, or the webpage was not visited yet. 21 | 46 |
47 | 48 | 49 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |

Crooked Style Sheets Website tracking/analytics without JS only with CSS

16 | 17 |
18 |

Link tests

19 | 20 | Link 1 21 | Link2 22 |
23 | Tracking even works if it is not an external link: 24 | Link 3 25 | 26 |
27 |

Hover test

28 | 29 | Hover about the fields with your mouse, so you can see how it can be tracked 30 | 31 | 32 | 33 | 36 | 37 | 40 | 41 | 42 | 45 | 46 | 49 | 50 |
34 |
Field 1
35 |
38 |
Field 2
39 |
43 |
Field 3
44 |
47 |
Field 4
48 |
51 | 52 |
53 | 54 |

Hover duration time Based on idea from jeyroik

55 | 56 |
Duration field
57 | 58 |
59 |

Input tests Note that you dont need to submit any form for data tracking

60 | Input "test" (without quotes) in the following text box and it will be detected: 61 | 62 |
63 | Check this box and it will be tracked 64 | 65 |
66 | 67 | 68 |
69 |
70 |
71 |
72 |
73 |
74 | 75 |
test
76 | 77 | Watch Results 78 | 79 | Reset results 80 | 81 |
82 | 83 | Github Repo 84 |
85 | 86 | -------------------------------------------------------------------------------- /README.zh.md: -------------------------------------------------------------------------------- 1 | **[这里](http://crookedss.bplaced.net/)你可以在这个回购中找到代码的演示. ** 2 | 3 | [中文版](./README.zh.md) 4 | 5 | # 歪曲的样式表 6 | 7 | 网站跟踪/分析只使用CSS和不使用JavaScript的概念验证. 8 | 9 | ## 我们可以用这个方法做什么? 10 | 11 | 我们可以收集关于用户的一些基本信息,比如屏幕分辨率(当浏览器被最大化时)以及使用哪个浏览器(或引擎). 此外,我们可以检测用户是否打开链接或者用鼠标悬停在元素上. 12 | 13 | 这可以用来跟踪用户访问哪些(外部)链接并使用悬停方法. 14 | 15 | ## 应该甚至可以跟踪用户如何移动鼠标(在页面背景中使用不可见的字段表). 16 | 17 | ### 但是,使用我的方法只能跟踪用户第一次访问链接或第一次在某个字段上盘旋. 18 | 19 | 也许有可能修改该方法,以便可以跟踪每次点击. 20 | 21 | 此外可以检测用户是否安装了特定的字体. 22 | 23 | ```CSS 24 | #link2:active::after { 25 | content: url("track.php?action=link2_clicked"); 26 | } 27 | ``` 28 | 29 | 基于这些信息,应该有可能检测用户使用哪个操作系统(因为不同的操作系统运送不同的字体,例如窗口上的"calibri"). 30 | 31 | ### 它是如何工作的? 32 | 33 | 大概的概念`在CSS中,您可以使用url("foo.bar")从外部资源添加图像;`属性. `有趣的是,这个资源只在需要的时候被加载(例如,当链接被点击时). `所以我们可以在css中创建一个选择器,当用户点击一个链接时调用一个特定的url: 34 | 35 | ```CSS 36 | @supports (-webkit-appearance:none) { 37 | #chrome_detect::after { 38 | content: url("track.php?action=browser_chrome"); 39 | } 40 | } 41 | ``` 42 | 43 | ### 在服务器端,一个php脚本会在调用url时保存时间戳. 44 | 45 | 浏览器检测 46 | 47 | ```CSS 48 | /** Font detection **/ 49 | @font-face { 50 | font-family: Font1; 51 | src: url("track.php?action=font1"); 52 | } 53 | 54 | #font_detection1 { 55 | font-family: Calibri, Font1; 56 | } 57 | ``` 58 | 59 | ### 浏览器检测是基于 60 | 61 | @supports媒体查询 62 | 63 | ```CSS 64 | @keyframes pulsate { 65 | 0% {background-image: url("track.php?duration=00")} 66 | 20% {background-image: url("track.php?duration=20")} 67 | 40% {background-image: url("track.php?duration=40")} 68 | 60% {background-image: url("track.php?duration=60")} 69 | 80% {background-image: url("track.php?duration=80")} 70 | 100% {background-image: url("track.php?duration=100")} 71 | } 72 | ``` 73 | 74 | ,我们检查一些浏览器特定的CSS属性 75 | 76 | ```CSS 77 | #duration:hover::after { 78 | -moz-animation: pulsate 5s infinite; 79 | -webkit-animation: pulsate 5s infinite; 80 | /*animation: pulsate 5s infinite;*/ 81 | animation-name: pulsate; 82 | animation-duration: 10s; 83 | content: url("track.php?duration=-1"); 84 | } 85 | ``` 86 | 87 | \-webkit-外观 88 | 89 | ### : 90 | 91 | 字体检测 92 | 93 | ```CSS 94 | #checkbox:checked { 95 | content: url("track.php?action=checkbox"); 96 | } 97 | ``` 98 | 99 | 对于字体检测,定义了一个新的字体系列. 100 | 101 | ```HTML 102 | 103 | ``` 104 | 105 | ```CSS 106 | #text_input:valid { 107 | background: green; 108 | background-image: url("track.php?action=text_input"); 109 | } 110 | ``` 111 | 112 | ## 那么就会尝试使用应该检查的字体的样式来判断文本是否存在. 113 | 114 | [当浏览器在用户系统上找不到字体时,定义的字体将被用作回退. ](http://crookedss.bplaced.net/)当发生这种情况时浏览器会尝试加载字体并在服务器上调用跟踪脚本. `测量悬停持续时间`对于悬停持续时间的方法(基于jeyroik的想法),我们定义了新的动画关键帧,每次请求一个新的关键帧就会请求一个url. `那么我们定义关键帧应该被用作div的动画. `我们可以选择动画的持续时间,这是我们可以测量的最大时间: 115 | 116 | 可以通过在关键帧集中插入更多步骤来增加持续时间测量的重新分配. 117 | 118 | 输入检测 119 | 120 | ## 检测用户是否检查复选框我们使用: css提供的选定选择器: 121 | 122 | 为了检测字符串"测试",我们结合了html模式属性,可以用来构建一些基本的输入验证. 123 | 124 | 结合: 有效的选择器,浏览器将请求我们的跟踪站点,当模式正则表达式与输入匹配时: 125 | 126 | 演示这里你可以在这个存储库中找到这些文件的演示. 该的index.html是使用此方法正在跟踪的文件. 参观results.php为跟踪的结果. 如果没有,或者在一个属性后面出现一个PHP警告,意味着这个属性的值是false,或者用户还没有访问过这个页面或者链接(是的,这有点脏,但是你可以看到方法). 此外,分辨率检测不能很好地工作,因为我只检测最常用的屏幕宽度. 此外,检测用户的实际屏幕高度有点棘手,因为css使用浏览器窗口的高度和东西比窗口的任务栏使得浏览器区域小于显示器. 你能做些什么来防止通过这种方法进行跟踪?目前我所知道的唯一方法是,完全禁用网页的CSS(你可以用像umatrix这样的插件来做到这一点). 几乎每一个现代网页都看起来很丑,没有CSS,甚至有时甚至是无法使用的问题. 所以禁用CSS是不是一个真正的选择,除非当你非常担心你的隐私(例如,当你使用Tor浏览器,你应该也许禁用CSS). 一个更好的解决方案是,浏览器不会加载外部内容(在CSS中引用),当需要时,但加载站点时. 那么就不可能检测出单一的行为. 这种对内容加载的修改可以通过浏览器本身来实现,也可以通过插件来实现(类似于noscript或者umatrix)问题是这个解决方案可能会对性能产生影响,因为浏览器必须加载大量的inital网站加载内容(也许浏览器根本不会使用内容). -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **[Here](http://crookedss.bplaced.net/) you can find a demo of the code in this repo.** 2 | 3 | [中文版](./README.zh.md) 4 | 5 | # Crooked Style Sheets 6 | 7 | Proof of concept for website tracking/analytics using only CSS and without Javascript. 8 | 9 | ## What can we do with this method? 10 | We can gather some basic information about the user, like the screen resolution (when the browser is maximized) and which browser (or engine) is used. 11 | Further we can detect if a user opens a link or hovers with the mouse over an element. This can be used to track which (external) links a user visits and using the hover method. It should be even possible to track how the user moved their mouse (using an invisible table of fields in the page background). However, using my method it's only possible to track when a user visits a link the first time or hovers over a field the first time. Maybe it's possible to modify the method so that it is possible to track every click. 12 | 13 | Furthermore it is possible to detect if a user has installed a specific font. Based on this information it should be possible to detect, which OS a users uses (because different operating systems ship different fonts, e.g. "Calibri" on Windows). 14 | 15 | ## How does it work? 16 | 17 | ### General idea 18 | 19 | In CSS you can add a image from an external resource using the url("foo.bar"); property. Interesting is, that this resource is only loaded when it is needed (for example when a link is clicked). 20 | 21 | So we can create a selector in CSS that calls a particular URL when the user clicks a link: 22 | 23 | ```CSS 24 | #link2:active::after { 25 | content: url("track.php?action=link2_clicked"); 26 | } 27 | ``` 28 | 29 | On the server side a PHP script saves the timestamp when the URL is called. 30 | 31 | ### Browser detection 32 | 33 | Browser detection is based on `@supports Media-Query`, and we check for some browser specific CSS property like `-webkit-appearance`: 34 | 35 | ```CSS 36 | @supports (-webkit-appearance:none) { 37 | #chrome_detect::after { 38 | content: url("track.php?action=browser_chrome"); 39 | } 40 | } 41 | ``` 42 | 43 | ### Font detection 44 | 45 | For font detection a new font family is defined. Then a text is tried to style with the font that should be checked if it exists. When the browser does not find the font on the user's system the defined font is used as a fallback. When this happens the browser tries to load the font and calls the tracking script on the server. 46 | 47 | ```CSS 48 | /** Font detection **/ 49 | @font-face { 50 | font-family: Font1; 51 | src: url("track.php?action=font1"); 52 | } 53 | 54 | #font_detection1 { 55 | font-family: Calibri, Font1; 56 | } 57 | ``` 58 | 59 | ### Measurement of hover duration 60 | 61 | For hover duration method (based on an idea by jeyroik), we define new animation keyframes, that will request a url, every time a new keyframe is requested: 62 | 63 | ```CSS 64 | @keyframes pulsate { 65 | 0% {background-image: url("track.php?duration=00")} 66 | 20% {background-image: url("track.php?duration=20")} 67 | 40% {background-image: url("track.php?duration=40")} 68 | 60% {background-image: url("track.php?duration=60")} 69 | 80% {background-image: url("track.php?duration=80")} 70 | 100% {background-image: url("track.php?duration=100")} 71 | } 72 | ``` 73 | Then we define that the keyframes should be used as animation for the div. There can we choose the duration of the animation, which is the maximum time we can measure: 74 | ```CSS 75 | #duration:hover::after { 76 | -moz-animation: pulsate 5s infinite; 77 | -webkit-animation: pulsate 5s infinite; 78 | /*animation: pulsate 5s infinite;*/ 79 | animation-name: pulsate; 80 | animation-duration: 10s; 81 | content: url("track.php?duration=-1"); 82 | } 83 | ``` 84 | 85 | The resoultion of the duration measurement can be increased, by insert more steps into the keyframes set. 86 | 87 | ### Input detection 88 | To detect if a user checks a checkbox we use the :selected Selector provided by CSS: 89 | ```CSS 90 | #checkbox:checked { 91 | content: url("track.php?action=checkbox"); 92 | } 93 | ``` 94 | 95 | For detection of the string "test" we combine the HTML pattern attribute, that can be used to build some basic input validation. In combination with the :valid selector, the browser will request our tracking site, when the pattern regex is matched by input: 96 | 97 | ```HTML 98 | 99 | ``` 100 | 101 | ``` CSS 102 | #text_input:valid { 103 | background: green; 104 | background-image: url("track.php?action=text_input"); 105 | } 106 | ``` 107 | 108 | 109 | ## Demo 110 | [Here](http://crookedss.bplaced.net/) you can find a demo of the files in this repository. The `index.html` is the file that is being tracked using this method. Visit the `results.php` for the results of the tracking. 111 | 112 | If nothing, or a PHP warning appears after a property, means that the value of this property is false, or that the user has not visited the page or link yet (Yeah, it's a bit dirty, but you can see the principle of the method). 113 | 114 | Also, resolution detection doesn't work so well yet, because I only have detection for the most used screen widths. Further, it is a bit tricky to detect the real screen height of the user, because CSS uses the height of the browser window and stuff than the Windows' task bar makes the browser area smaller than the monitor. 115 | 116 | ## What can you do to prevent tracking via this method? 117 | The only way that is known to me currently is, to disable CSS for a webpage completly (you can do this with a plugin like uMatrix). The problem that almost every modern webpage looks very ugly without CSS and is sometimes even unusable completly. So disable CSS is not a real option, except when you are very worried about your privacy (for example, when you are using Tor browser, you should maybe disable CSS). 118 | 119 | A better solution would be, that browsers does not load the external content (referenced in CSS), when it is needed, but when the site is loaded. Then it would be impossible to detect single actions. This modification to content loading could be implemented by the browsers itself, or maybe by a plugin (similar to NoScript or uMatrix) 120 | 121 | The problem is that this solution maybe have a performance impact, because the browser has to load much content on inital site loading (and maybe the browser will not use the content at all). 122 | 123 | -------------------------------------------------------------------------------- /screen.css: -------------------------------------------------------------------------------- 1 | /** Styling rules **/ 2 | .field { 3 | background-color:grey; 4 | } 5 | 6 | .field:hover { 7 | background-color: red; 8 | } 9 | 10 | /** Tracking rules **/ 11 | #type { 12 | content: url("track.php?action=screen_start"); 13 | } 14 | 15 | #link1:active::after { 16 | content: url("track.php?action=link1_clicked"); 17 | } 18 | 19 | /* 20 | #link1:hover::after { 21 | content: url("track.php?action=link1_hover_start"); 22 | }*/ 23 | 24 | #link2:active::after { 25 | content: url("track.php?action=link2_clicked"); 26 | } 27 | 28 | #link3:active::after { 29 | content: url("track.php?action=link3_clicked"); 30 | } 31 | 32 | @-webkit-keyframes pulsate { 33 | 0% {background-image: url("track.php?duration=00")} 34 | 20% {background-image: url("track.php?duration=20")} 35 | 40% {background-image: url("track.php?duration=40")} 36 | 60% {background-image: url("track.php?duration=60")} 37 | 80% {background-image: url("track.php?duration=80")} 38 | 100% {background-image: url("track.php?duration=100")} 39 | } 40 | @-moz-keyframes pulsate { 41 | 0% {background-image: url("track.php?duration=00")} 42 | 20% {background-image: url("track.php?duration=20")} 43 | 40% {background-image: url("track.php?duration=40")} 44 | 60% {background-image: url("track.php?duration=60")} 45 | 80% {background-image: url("track.php?duration=80")} 46 | 100% {background-image: url("track.php?duration=100")} 47 | } 48 | @keyframes pulsate { 49 | 0% {background-image: url("track.php?duration=00")} 50 | 10% {background-image: url("track.php?duration=10")} 51 | 20% {background-image: url("track.php?duration=20")} 52 | 30% {background-image: url("track.php?duration=30")} 53 | 40% {background-image: url("track.php?duration=40")} 54 | 50% {background-image: url("track.php?duration=50")} 55 | 60% {background-image: url("track.php?duration=60")} 56 | 70% {background-image: url("track.php?duration=70")} 57 | 80% {background-image: url("track.php?duration=80")} 58 | 90% {background-image: url("track.php?duration=90")} 59 | 100% {background-image: url("track.php?duration=100")} 60 | } 61 | 62 | #duration:hover::after { 63 | -moz-animation: pulsate 5s infinite; 64 | -webkit-animation: pulsate 5s infinite; 65 | /*animation: pulsate 5s infinite;*/ 66 | animation-name: pulsate; 67 | animation-duration: 10s; 68 | animation-iteration-count: infinite; 69 | content: url("track.php?duration=-1"); 70 | } 71 | 72 | #s1:hover::after { 73 | content: url("track.php?action=s1_hovered"); 74 | } 75 | 76 | #s2:hover::after { 77 | content: url("track.php?action=s2_hovered"); 78 | } 79 | 80 | #s3:hover::after { 81 | content: url("track.php?action=s3_hovered"); 82 | } 83 | 84 | #s4:hover::after { 85 | content: url("track.php?action=s4_hovered"); 86 | } 87 | 88 | /* Taken from http://browserhacks.com/ */ 89 | @supports (-webkit-appearance:none) { 90 | #chrome_detect::after { 91 | content: url("track.php?action=browser_chrome"); 92 | } 93 | } 94 | 95 | @supports (-moz-appearance:meterbar) { 96 | #firefox_detect::after { 97 | content: url("track.php?action=browser_firefox"); 98 | } 99 | } 100 | 101 | 102 | /** Font detection **/ 103 | @font-face { 104 | font-family: Font1; 105 | src: url("track.php?action=font1"); 106 | } 107 | 108 | #font_detection1 { 109 | font-family: Calibri, Font1; 110 | } 111 | 112 | 113 | /** input tests */ 114 | #text_input:valid { 115 | background: green; 116 | background-image: url("track.php?action=text_input"); 117 | } 118 | 119 | #checkbox:checked { 120 | content: url("track.php?action=checkbox"); 121 | } 122 | 123 | /** Orientation detection **/ 124 | 125 | @media (orientation: portrait) { 126 | #orientation { 127 | content: url("track.php?orientation=portrait"); 128 | } 129 | } 130 | 131 | @media (orientation: landscape) { 132 | #orientation { 133 | content: url("track.php?orientation=landscape"); 134 | } 135 | } 136 | 137 | /* Width part of the Resolution */ 138 | 139 | @media (min-device-width: 240px) { 140 | #width { 141 | content: url("track.php?width=360"); 142 | } 143 | } 144 | 145 | 146 | @media (min-device-width: 360px) { 147 | #width { 148 | content: url("track.php?width=360"); 149 | } 150 | } 151 | 152 | @media (min-device-width: 480px) { 153 | #width { 154 | content: url("track.php?width=480"); 155 | } 156 | } 157 | 158 | @media (min-device-width: 640px) { 159 | #width { 160 | content: url("track.php?width=640"); 161 | } 162 | } 163 | 164 | @media (min-device-width: 720px) { 165 | #width { 166 | content: url("track.php?width=720"); 167 | } 168 | } 169 | 170 | @media (min-device-width: 1024px) { 171 | #width { 172 | content: url("track.php?width=1024"); 173 | } 174 | } 175 | 176 | @media (min-device-width: 1280px) { 177 | #width { 178 | content: url("track.php?width=1280"); 179 | } 180 | } 181 | 182 | @media (min-device-width: 1366px) { 183 | #width { 184 | content: url("track.php?width=1366"); 185 | } 186 | } 187 | 188 | @media (min-device-width: 1920px) { 189 | #width { 190 | content: url("track.php?width=1920"); 191 | } 192 | } 193 | 194 | @media (min-device-width: 3840px) { 195 | #width { 196 | content: url("track.php?width=3840"); 197 | } 198 | } 199 | 200 | @media (min-device-width: 4096px) { 201 | #width { 202 | content: url("track.php?width=4096"); 203 | } 204 | } 205 | 206 | /* Height part of the resolution */ 207 | 208 | @media (min-device-height: 240px) { 209 | #height { 210 | content: url("track.php?height=240"); 211 | } 212 | } 213 | 214 | 215 | @media (min-device-height: 320px) { 216 | #height { 217 | content: url("track.php?height=320"); 218 | } 219 | } 220 | 221 | @media (min-device-height: 360px) { 222 | #height { 223 | content: url("track.php?height=360"); 224 | } 225 | } 226 | 227 | @media (min-device-height: 480px) { 228 | #height { 229 | content: url("track.php?height=480"); 230 | } 231 | } 232 | 233 | @media (min-device-height: 504px) { 234 | #height { 235 | content: url("track.php?height=504"); 236 | } 237 | } 238 | 239 | @media (min-device-height: 540px) { 240 | #height { 241 | content: url("track.php?height=540"); 242 | } 243 | } 244 | 245 | @media (min-device-height: 568px) { 246 | #height { 247 | content: url("track.php?height=568"); 248 | } 249 | } 250 | 251 | @media (min-device-height: 576px) { 252 | #height { 253 | content: url("track.php?height=576"); 254 | } 255 | } 256 | 257 | @media (min-device-height: 600px) { 258 | #height { 259 | content: url("track.php?height=600"); 260 | } 261 | } 262 | 263 | @media (min-device-height: 640px) { 264 | #height { 265 | content: url("track.php?height=640"); 266 | } 267 | } 268 | 269 | @media (min-device-height: 667px) { 270 | #height { 271 | content: url("track.php?height=667"); 272 | } 273 | } 274 | 275 | @media (min-device-height: 690px) { 276 | #height { 277 | content: url("track.php?height=690"); 278 | } 279 | } 280 | 281 | @media (min-device-height: 720px) { 282 | #height { 283 | content: url("track.php?height=720"); 284 | } 285 | } 286 | 287 | @media (min-device-height: 768px) { 288 | #height{ 289 | content: url("track.php?height=768"); 290 | } 291 | } 292 | 293 | @media (min-device-height: 1024px) { 294 | #height { 295 | content: url("track.php?height=1024"); 296 | } 297 | } 298 | 299 | @media (min-device-height: 1080px) { 300 | #height { 301 | content: url("track.php?height=1080"); 302 | } 303 | } 304 | 305 | @media (min-device-height: 2160px) { 306 | #height { 307 | content: url("track.php?height=2160"); 308 | } 309 | } 310 | 311 | 312 | @media (min-device-height: 1366px) { 313 | #height { 314 | content: url("track.php?height=1366"); 315 | } 316 | } 317 | 318 | --------------------------------------------------------------------------------