Add Pwned Passwords HTTP Headers

Problem this snippet solves:

This is an example snippet which uses Troy Hunt’s ‘Pwned Passwords’ API that can be used to intercept a request passing the BIG-IP. 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

F5-Password-Pwned
, with either the value
Yes
or
No
depending on whether the password being handled is found in the database or not. It also adds an additional HTTP header
F5-Password-Pwned-Score
. This header will hold an integer that represents the number of different date breaches in which this password was found.

The POST request is then passed on to the origin server for handling, with the extra headers 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.

It’s good to note that the password itself will not be shared while using this API. This snippet uses a mathematical property called k-anonymity. For more information about k-anonymity and Troy Hunt’s ‘Pwned Passwords’ API see:

https://www.troyhunt.com/ive-just-launched-pwned-passwords-version-2/

This idea for making this example snippet was inspired by this blog article by John Graham-Cumming:

https://blog.cloudflare.com/using-cloudflare-workers-to-identify-pwned-passwords/

This snippet also uses Patt-tom McDonnell’s hibp-checker node package.

Examples

In the example below you see that the 'topsecret' password has been found in the database.

$ curl  -X POST -d 'password=topsecret' http://10.23.98.35/headers.php 
HTTP headers received:

User-Agent: curl/7.40.0
Host: 10.23.98.35
Accept: */*
Content-Length: 18
Content-Type: application/x-www-form-urlencoded
X-Forwarded-For: 10.23.92.2
F5-Password-Pwned: Yes
F5-Password-Pwned-Score: 15279
$

The next example shows a more secure password that isn't in the database.

$ curl  -X POST -d 'password=llo5lFvXCEc4ZYruQmmt' http://10.23.98.35/headers.php          
HTTP headers received:

User-Agent: curl/7.40.0
Host: 10.23.98.35
Accept: */*
Content-Length: 29
Content-Type: application/x-www-form-urlencoded
X-Forwarded-For: 10.23.92.2
F5-Password-Pwned: No
F5-Password-Pwned-Score: 0
$ 

How to use this snippet:

Prepare the BIG-IP

  • Provision the BIG-IP with iRuleLX.
  • Create LX Workspace: Local Traffic > LX Workspaces

    • Name: workspace_hibp-headers
    • Add Extension: extension_hibp-headers
  • Create LX Plugin: Local Traffic > iRules > LX Plugins

    • Name: plugin_hibp-headers
    • From Workspace: workspace_hibp-headers
  • Create iRules LX Profile: Local Traffic > Profiles > Other > iRules LX

    • Name: ilx_hibp-headers
    • Plugin: plugin_hibp-headers

Install the node.js hibp-checker, querystring and utf8 module

# cd /var/ilx/workspaces/Common/workspace_hibp-headers/extensions/extension_hibp-headers/
# npm install hibp-checker querystring utf8 --save
/var/ilx/workspaces/Common/workspace_hibp-headers/extensions/extension_hibp-headers/
├── hibp-checker@1.0.0 
├── querystring@0.2.0 
└── utf8@3.0.0
#

Add iRules LX Profile to Virtual Server

  • Select Virtual Server: Local Traffic > Virtual Servers > some_vs
  • Select Advanced Configuration.
  • Select ilx_http-headers as iRule LX Profile.

Code :

var f5 = require('f5-nodejs');
var plugin = new f5.ILXPlugin();
var qs = require('querystring');
var utf8 = require('utf8');
var hibpChecker = require('hibp-checker');

plugin.on("connect", function(flow) {
    
    var hibpEngineEnable = 0;
    var body = '';

    flow.client.on("requestStart", function(request) {

        if ((request.params.method == "POST") && (request.params.headers['content-length'] > 0) && (request.params.headers['content-length'] <= 1048576)) {
            hibpEngineEnable = 1;
        }

    });

    flow.client.on("readable", function() {
        while (true) {
            var buffer = flow.client.read();
            if (buffer !== null) {
                if (hibpEngineEnable == 1) {
                    body += buffer;
                }
                else {
                    flow.server.write(buffer);
                }
            }
            else {
                break;
            }
        }
    });

    flow.client.on("requestComplete", function(request) {
        if ( hibpEngineEnable == 1 ) {
            var post = qs.parse(body);
            if( post.password ) {
                const password = utf8.encode(post.password);
                var breachCount = hibpChecker(password);
    
                breachCount.then(function(result) {
                    if(result > 0) {
                        request.setHeader('F5-Password-Pwned', 'Yes');
                    } else {
                        request.setHeader('F5-Password-Pwned', 'No');
                    }
                    request.setHeader('F5-Password-Pwned-Score', result);
                    flow.server.write(body);
                    request.complete();
                }, function(err) {
                    console.log('ERROR: ' + err);
                    flow.server.write(body);
                    request.complete();
                });
            }
        }
        else {
            request.complete();
        }
    });


    // Register callbacks for error events. Errors events must be caught.
    flow.client.on("error", function(errorText) {
        console.log("client error event: " + errorText);
    });
    flow.server.on("error", function(errorText) {
        console.log("server error event: " + errorText);
    });
    flow.on("error", function(errorText) {
        console.log("flow error event: " + errorText);
    });
});


// Start listening for new flows.
var options = new f5.ILXPluginOptions();
options.handleServerData = false;
options.handleServerResponse = false;
plugin.start(options);

Tested this on version:

13.0
Updated Jun 06, 2023
Version 2.0

Was this article helpful?

No CommentsBe the first to comment