Using Cloudflare Workers to identify pwned passwords


Last week Troy Hunt launched his Pwned Password v2 service which has an API handled and cached by Cloudflare using a clever anonymity scheme.

The following simple code can check if a password exists in Troy’s database without sending the password to Troy. The details of how it works are found in the blog post above.

use strict;
use warnings;

use LWP::Simple qw/$ua get/;
$ua->agent('Cloudflare Test/0.1');
use Digest::SHA1 qw/sha1_hex/;

uc(sha1_hex($ARGV[0]))=~/^(.{5})(.+)/;
print get("https://api.pwnedpasswords.com/range/$1")=~/$2/?'Pwned':'Ok', "n";

It’s just as easy to implement the same check in other languages, such as JavaScript, which made me realize that I could incorporate the check into a Cloudflare Worker. With a little help from people who know JavaScript far better than me, I wrote the following Worker:

addEventListener('fetch', event => {
  event.respondWith(fetchAndCheckPassword(event.request))
})

async function fetchAndCheckPassword(req) {
  if (req.method == "POST") {
    try {
      const post = await req.formData()
      const pwd = post.get('password')
      const enc = new TextEncoder("utf-8").encode(pwd)

      let hash = await crypto.subtle.digest("SHA-1", enc)
      let hashStr = hex(hash).toUpperCase()
      
      const prefix = hashStr.substring(0, 5)
      const suffix = hashStr.substring(5)

      const pwndpwds = await fetch('https://api.pwnedpasswords.com/range/' + prefix)
      const t = await pwndpwds.text()
      const pwnd = t.includes(suffix)

      let newHdrs = new Headers(req.headers)
      newHdrs.set('Cf-Password-Pwnd', pwnd?'YES':'NO')

      const init = {
        method: 'POST',
        headers: newHdrs,
        body: post
      }

      return await fetch(req.url, init)    
    } catch (err) {
      return new Response('Internal Error')
    }
  }
  
  return await fetch(req)
}

function hex(a) {
  var h = ""
  var b = new Uint8Array(a)
  for(var i = 0; i < b.length; i++){
    var hi = b[i].toString(16)
    h += hi.length === 1?"0"+hi:hi
  }
  return h
}

This Worker can be used to intercept a request passing through Cloudflare to a Cloudflare site. It looks at POST requests and extracts a field called password and checks it against Troy Hunt’s service.

It then adds an HTTP request header, Cf-Password-Pwned, with either the value YES or NO depending on whether the password being handled is found in the database or not.

The POST request is then passed on to the origin server for handling, with the extra header inserted. This could, for example, be used on a signup page to check whether the password a user is hoping to use has already been found in a leak. The server would simply look at the header.

Clearly, this code isn’t completely production ready (it does a bad job of handling failure, for example), but it gives a good idea of the power of a Cloudflare Worker to perform a subrequest to an API as part of normal request processing by Cloudflare and augment at request with information.

Trying it out

To test it out I created a simple page that just returns the received HTTP request headers as text and deployed this as a ‘signup’ page with the Worker code above routed to it.

And checked the a simple GET request was not handled by the Worker (notice that the Cf-Password-Pwned header is not present.

$ curl https://signup.example.com
Host: signup.example.com
Connection: Keep-Alive
Accept-Encoding: gzip
Cf-Ipcountry: US
Cf-Ray: 3f329308132f92b8-SJC
X-Forwarded-Proto: https
Cf-Visitor: {"scheme":"https"}
Accept: */*
User-Agent: curl/7.26.0

But a POST request with a password results in an extra header. Clearly no one should be using the password 12345.

$ curl -X POST -d 'password=12345' https://signup.example.com
Host: signup.example.com
Connection: Keep-Alive
Accept-Encoding: gzip
Cf-Ipcountry: US
Cf-Ray: 3f3294e714a36d42-SJC
Content-Length: 130
X-Forwarded-Proto: https
Cf-Visitor: {"scheme":"https"}
Content-Type: application/x-www-form-urlencoded
Accept: */*
Cf-Password-Pwnd: YES
User-Agent: curl/7.26.0

But it looks like the password kRc4qMwAtexDVZVygPnSt7LP5jPFsUDt is safe for the time being:

$ curl -X POST -d 'password=kRc4qMwAtexDVZVygPnSt7LP5jPFsUDt' https://signup.example.com
Host: signup.example.com
Connection: Keep-Alive 
Accept-Encoding: gzip
Cf-Ipcountry: US
Cf-Ray: 3f329675e7f29625-SJC
Content-Length: 157
X-Forwarded-Proto: https
Cf-Visitor: {"scheme":"https"}
Content-Type: application/x-www-form-urlencoded
Accept: */*
Cf-Password-Pwnd: NO
User-Agent: curl/7.26.0

The power of Cloudflare Workers comes from the ability to run standard JavaScript written against the Service Workers API on Cloudflare’s edge nodes around the world. Small snippets of code can be used to transform and enhance requests and responses, build responses from multiple API calls, and interact with the Cloudflare cache.

Read more in the developer documentation.



Source link