Browsers – Broken By Design

I set out a few hours ago to write about a problem I had experienced in Safari, Chrome, Firefox, and Internet Explorer. Opera worked as I expected, which had me convinced the other vendors merely dropped the ball, and shipped partial implementations for the :visited and :link pseudo-classes.

My CSS was very simple:

:link {
    color: white;
    background: red;
}

:link::before {
    content: "Pseudo: ";
}

A color set for unvisited links, followed by some content in the ::before pseudo element on all unvisited links. So when I dropped in two links (one visited, one not) I expected to see the following:

image00 copy

Instead, what I saw was a litter of inconsistent results between Chrome, Safari, Internet Explorer, Firefox, and Opera. The above image came from Opera. It performed exactly as I suspected. The others, not so much.

Chrome and Firefox gave similar results; both set “Pseudo” as a virtual first-child of both anchors (though I only asked to have it on unvisited links), and leaked the background color from :link into :visited.

Internet Explorer did better than Chrome and Firefox; it preserved the “pseudo” text, but left the background of the visited link untouched. I was still very perplexed as to why it was spilling values from :link over into :visited.

You can observe something interesting in Internet Explorer – :visited links start visually identical to :link links, but are transitioned into their :visited state immediately thereafter. Setting a transition on all links reveals the change when the document loads:

a {
    transition: background 10s;
}

You’ll see the :link links remain unchanged but :visited links slowly adopt their end style. This is not the case for Chrome, Firefox, Opera, or Safari.

Safari appeared to be the most broken of them all. It duplicated everything. Further, I attempted to set additional styles to :visited, and it completely ignored them.

Discovering History

I found it incredible that all of these browsers, with the exception of Opera, would get this so wrong. So like any good developer I took to the web to see who else might be complaining about this, which is when I came across a Stack Overflow post suggesting this was somehow related to security concerns.

This offered another search vector; security issues. That was the needed catalyst for opening up a fascinating story about yet another creative attempt by developers to put their nose where it may not belong – in the client browser’s history.

Browsers typically style links with a default color to indicate whether it is a visited link or not. We’ve all seen those blue and purple links before – they’ve become quite at home on the web. Somebody got the (seriously) brilliant idea to create anchors ad hoc, and use getComputedStyle to see if the links represented visited or unvisited locations.

Mozilla reports that you could test more than 200,000 urls every minute with this approach. As such, you could – with a great deal of accuracy – fingerprint the user visiting your site based on the other urls they have visited; and browser history runs deep.

Scorched Earth Solution

The solution implemented by Firefox (and apparently others) was to greatly reduce the presence of visited links. They started by instructing functions like getComputedStyle and querySelectorAll to lie (their words, not mine) about their results. Sure enough, a simple check confirms that though my :visited links have a different font color than :link links, getComputedStyle says they’re the same rgb value.

Mozilla’s Christopher Blizzard continues with a very key point (emphasis added):

You will still be able to visually style visited links, but you’re severely limited in what you can use. We’re limiting the CSS properties that can be used to style visited links to color, background-color, border-*-color, and outline-color and the color parts of the fill and stroke properties. For any other parts of the style for visited links, the style for unvisited links is used instead. In addition, for the list of properties you can change above, you won’t be able to set rgba() or hsla() colors or transparent on them.

And there it was, the explanation that tied all of the seemingly broken pieces together. The browser would instruct :visited to share property values with :link. The browsers aren’t broken; they were designed to fail.

Test Confirmations

I wanted to explore the now-understood implementation a bit further, so I tried a few things. Querying the DOM for :visited links (expecting 1 of 2) was my first decision. I also queried the DOM for :link as well – expecting 2 due to the new security:

Browser Number of :visited Number of :link
Chrome 0 2
Firefox 0 2
Internet Explorer 0 2
Safari 0 2
Opera 1 2

Nearly all of the browsers report no :visited link, with the exception of Opera. All browsers report a total of 2 links when querying :link.

So it seems like you can still get a list of visited sites in Opera. Internet Explorer, Firefox, Chrome, and Safari all prevented the exploit from being carried out. But one thing about Internet Explorer intrigued me; remember the transitioning I spoke of earlier?

Internet Explorer Still Vulnerable?

I noticed that if I set transition: background 10s on anchors in Internet Explorer you would slowly see all visited links slowly tween into their end state. Transitions fire the transitionend event when they complete, so could we still get the user’s visited links in Internet Explorer?

The CSS to test this is very simple:

a { transition: background .01s }
:link { background: red }

And the following JavaScript:

/* Anchor var, fragment, and array of guesses */
var anchor;
var fragment = document.createDocumentFragment();
var websites = [
    'http://bing.com',
    'http://google.com',
    'http://jquery.com',
    'http://appendto.com',
    'http://stackoverflow.com'
];

/* Listen for completed transitions */
document.addEventListener("transitionend", function (event){
    console.log(event.target.href);
}, false);

/* Create a link for each url */
websites.forEach(function(url){
    anchor = document.createElement("a");
    anchor.setAttribute("href", url);
    anchor.innerHTML = url;
    fragment.appendChild( anchor );
});

/* Add our document fragment to our DOM */
document.body.appendChild(fragment);

Immediately upon being added to the DOM our :visited anchors will start to transition away from looking like a :link anchor. Once that transition is complete, we learn what the URL is, and confirm that the user has visited that site.

Closing Thoughts

I was reminded today just how exciting our industry is. People are constantly experimenting, learning new things, and sharing that knowledge. Industry experts, developers, designers, and browser vendors are always working to shift and adjust in light of the ever-evolving web.

Although the CSS2.1 Specification says something, that doesn’t make it so – even if the feature is 15 years old. If the ends justify the means, browser vendors will (and did) fire-bomb their own code-base to protect the end user.

Finally, we’re all in this together; we ought to work together. Microsoft launched the @IEDevChat twitter account not too long ago to assist developer’s who are attempting to follow web-standards. Then even organized a group of developers who wanted to volunteer and help build a better web; you can find (and join) it online at http://useragents.ie.

I’m sure there’s more history here that I’ve yet to discover, but what I have seen is enough to pique my interest. If you have anything else to share, or can contribute to the vulnerability test approach above, please leave a comment below. I may swing back around and check out the other browsers later on.