Inspired by Anubis last week, I came up with an idea to detect if a client is a browser using CSS animations and image lazy loading. I spent a few hours writing a simple PoC, and it works!

How it works?

CSSWAF places random hidden empty.gif images in CSS animation progress, allowing the browser to play these images one by one in order.

...
<style>
@keyframes csswaf-load {
  ` + func(expectedSequence []string) string {
        lines := []string{}
        for i, img := range expectedSequence {
            f := float64(i) / float64(len(expectedSequence))
            lines = append(lines, strconv.Itoa(int(f*100))+`% { content: url('/_csswaf/img/`+img+`?sid=`+sessionID+`'); }`)
        }
        lines = shuffle(lines)
        return strings.Join(lines, "\n")
    }(expectedSequence) + `
}
.csswaf-hidden {
width: 1px;
height: 1px;
position: absolute;
top: 0px;
left: 0px;
animation: csswaf-load ` + strconv.FormatFloat(cssAnimationTS, 'f', -1, 64) + `s linear infinite;
}
</style>
...
<div class="csswaf-hidden"></div>

The backend measures the loading order. If the loading order is correct, it passes the request to the target server. Otherwise, 🙅.

...
// Check if sequence matches expected sequence
expectedSeqTTL := st.expected.Get(sessionID)
var expectedSeq []string
if expectedSeqTTL != nil {
    expectedSeq = expectedSeqTTL.Value()
}
if expectedSeq != nil && len(sequence) == len(expectedSeq) {
    match := true
    for i := range sequence {
        if (sequence)[i] != (expectedSeq)[i] {
            match = false
            break
        }
    }
    st.validated.Set(sessionID, match, ttlcache.DefaultTTL)
...

HoneyPot

CSSWAF places some honeypot empty.gif files in HTML <img> tags but instructs the browser not to load them. If someone loads the honeypot GIFs, 🙅.

lines = append(lines, `<img src="/_csswaf/img/`+img+`?sid=`+sessionID+`" style="width: 0px; height: 0px; position: absolute; top: -9999px; left: -9999px;" loading="lazy">`)

CSSWAF also places some unvisible <a> tags in HTML, if someone clicks the honeypot links, 🙅.

.honeya {
    display: none;
    width: 0px;
    height: 0px;
    position: absolute;
    top: -9898px;
    left: -9898px;
}

lines = append(lines, "<a href='/_csswaf/img/"+img+"?sid="+sessionID+"' class='honeya'>View Content</a>")