As the web gets more and more capable, developers are able to make richer online experiences. There are times, however, where some new web capabilities may not work as you would expect in the interest of usability, security and privacy.
I have run into situations like this. Like lazy loading in HTML. It’s easy to drop that attribute onto an image element only to realize… it actually needs more than that to do its thing. We’ll get into that specific one in a moment as we look at a few other features that might not work exactly as you‘d expect.
:visited links have limited styling and
getComputedStyle lies about their style
This limitation has been around for a while, but it does show how browser features can be exploited. One possible exploit is an anchor gets some
href value and see if a particular
href causes the link to appear visited—reconstructing a user’s history in the process.
These days, attempting to use
getComputedStyle on a
:visited link returns the style of the unvisited (
:link) link instead. That’s just one of those things you have to know because that’s different from how it intuitively ought to work.
There are two approaches ways you could try to get around this, but neither of them are possible.
- make the visited link’s style trigger a side effect (e.g. a layout shift), or
- leverage the sibling (
+) or child (
>) CSS selectors to render another style.
Regarding side effects, while there are some clever yet fragile ways to do this, the options we have for styling
:visited links are limited and some styles (like
background-color) will only work if they’re applied to unvisited links. As for using a sibling or child, executing
getComputedStyle on these returns the style as if the link wasn’t visited to begin with.
Browsers don’t cache assets across sites anymore
One advantage of a CDN was that they allowed for a particular resource (like Google Fonts) to be cached in the browser for use across different websites. While this does provide a big performance win, it has grave privacy implications.
Given that an asset that’s already cached will take longer to load than one that’s not, a site could perform a timing attack to not only see your site history but also expose both who you are and your online activity. Jeff Kaufman gives an example:
Unfortunately, a shared cache enables a privacy leak. Summary of the simplest version:
- I want to know if you’re a moderator on www.forum.example.
- I know that only pages under
- When you visit my page I load
www.forum.example/moderators/header.cssand see if it came from cache.
In light of this, browsers don’t offer this anymore.
performance.now() may be inaccurate
A scary group of vulnerabilities came out as couple of years ago, one of which was called Spectre. For an in depth explanation, see Google’s leaky.page (works best in Chromium) as a proof of concept. But for the purposes of this article, just know that the exploit relies on getting highly accurate timing, which is something that
performance.now() provides, to try and map sensitive CPU data.
To mitigate Spectre, browsers have reduced its accuracy and may add noise as well. These range from 20μs to 1ms and can be changed based on various conditions like HTTP headers and browser settings.
Lazy loading with the
onscroll. Except for Safari, we can apply the
loading attribute to images and iframes (in Chromium) and the browser will handle lazy loading.
Note that lazy loading can’t be polyfilled since an image is probably loading by the time you check for the
loading attribute’s support.
- If scripting is disabled for an element, return false.
This is an anti-tracking measure, because if a user agent supported lazy loading when scripting is disabled, it would still be possible for a site to track a user’s approximate scroll position throughout a session, by strategically placing images in a page’s markup such that a server can track how many images are requested and when.
Browsers can limit features based on user preferences
Some users might opt to heavily restrict browser functionality in the interest of further security and privacy. Firefox and Tor are two browsers that do this through the resist fingerprint setting which does things like reducing the precision of certain variables (dimensions and time), omitting certain variables entirely, limiting or disabling some Web APIs and never matching media queries. WebKit has a document outlining how browsers can approach fingerprint resistance.
Note that this goes beyond the standard anti-tracking features that browsers implement. It’s unlikely that a user will enable this as they would need a very specific threat model to do so. Part of this can be countered with progressive enhancement, graceful degradation, and understanding your users. This limitation is a big issue when you actually need fingerprinting, like fraud detection. So, if it’s absolutely necessary, look for an alternative means.
Screen readers might not relay the semantics of certain elements
Semantic HTML is great for many reasons, most notably that it conveys meaning in markup that software, like screen readers, interpret and announce to users who rely on them to navigate the web. It’s essential for crafting accessible websites. But, at times, those semantics aren’t conveyed—at least how you might expect. Something might be accessible, but still have usability issues.
An example is the way removing a list’s markers removes its semantic meaning in WebKit with VoiceOver enabled. It’s a very common pattern, most notably for site navigation. Apple Accessibility Standards Manager James Craig explains why it’s a usability issue, though, citing the W3C’s Design Principle of Priority of Constituents:
In case of conflict, consider users over authors over implementors over specifiers over theoretical purity. In other words costs or difficulties to the user should be given more weight than costs to authors;
Another case where semantics might not be relayed is with emphasis. Take inline elements like
data—all elements that have semantic meanings, but are unlikely to be read out because they can get noisy. This can be changed in a user’s screenreader’s settings, but if you really want it to be read you can declare it in visually hidden in the
content property of either a
To illustrate this I made a brief example to see how NVDA with Firefox 89 and VoiceOver with Safari 14.6 read out semantic elements.
Unlike VoiceOver, NVDA reads out some of the semantic elements (
mark) and tries to emphasize text by gradually increasing the volume of emphasized text. Both of them have no trouble reading out the
:before/:after psudo-elements however. Also, VoiceOver read out the tag’s brackets (greater than, less than), though both screenreaders have the ability to change how much punctuation is read.
To see whether or not you need to emphasize the emphasis, make sure you test with your users and see what they need. I didn’t focus on the visual aspect but the default styling of emphasis elements may be inconsistent across browsers, so make sure you provide suitable styling to go along with it.
Web storage might not be persistent
The WHATWG Web storage specification includes a section on privacy that outlines possible ways to prevent storage from being a tracking vector. One such way is to make the data expire. This is why Safari controversially limits script writable storage for seven days. Note that this doesn’t apply to “installed” websites added to the home screen.
Interesting, isn’t it? Some web features that we might expect to work a certain way just don’t. That isn’t to say that the features are wrong and need to be fixed, but more of a heads up as we write code.
It’s worth examining your own assumptions during development. Critically examine what your users need and factor it in as you make your site. You’re certainly welcome to work around these these as you encounter them, but in cases where you’re unable to, make sure to find and provide reasonable progressive enhancement and graceful degradation. It’s OK if users don’t experience a website the exact same way in every browser as long as they’re able to do what they need to.
That’s my list of things that don’t work the way I expect them to. What’s on your list? I’m sure you’ve got some and I’d love to see them in the comments!