The NIST's Digital Identity Guidelines requires passwords to be compared to passwords obtained from previous breaches. The best way to do that is with Troy Hunt's Have I Been Pwned service, which offers an online password search as well as an API.
The code in this repository offers a simple container image to run web service that will search a local copy of the HIBP password hashes. It predates the online HIBP API, which is the best way to search for breached passwords, but this may still be useful to anyone that prefers a local service.
To use this image, you need a downloaded copy of Troy's Pwned Password list from https://haveibeenpwned.com/Passwords. This contains ordered lists of SHA-1 or NTLM hashes and a count of occurences for breached passwords.
As of May 2022, the best way to get the most up to date passwords is to use
the Pwned Passwords downloader.
You can download this as a single file or as smaller individual .txt
files.
You can provide either SHA-1 or NTLM hashes, or both. Obviously only the functions for which you provide data will work ;-)
If using the individual hash .txt
files, you need to mount the directory
containing the files to /data/sha1
and/or /data/ntlm
in the container.
Assuming you're in a directory containing a sha1
and/or ntlm
subdirectory,
you can run:
podman run --rm -t \
-v $PWD:/data \
-p 2080:8080 \
pwned-httpd
If you downloaded the hashes into a single file, you also need to provide
the file name in the $FILENAME
environment variable, for example:
podman run --rm -t \
-v $PWD:/data \
-p 2080:8080 \
-e SHA1_FILENAME=sha1.txt \
-e NTLM_FILENAME=ntlm.txt \
pwned-httpd
It exposes 3 URLs, namely:
/sha1hash/{password}
returns the SHA1 hash of a password.
Example:
$ curl http://0.0.0.0:2080/sha1hash/abcde
03DE6C570BFE24BFC328CCD7CA46B76EADAF4334
This is for convenience / testing only. It would be safer to do this offline :-) Many options exist for doing this. One example is this Python snippet:
python -c 'import hashlib; print(hashlib.sha1("Password".encode()).hexdigest());'
/pwnedsha1/{sha1hash}
checks if the password with this SHA1 hash has been breached.
The HTTP status code indicates whether a match is found. Possible status codes are:
- 400 Bad Request: returned if we can't find the data file, or you don't specify a hash
- 202 OK: we found the hash - the response body contains the match count
- 404 Not found: No match found in breach database
Example 1 - weak password "abcde" has 51725 matches:
$ curl -v http://0.0.0.0:2080/pwnedsha1/03DE6C570BFE24BFC328CCD7CA46B76EADAF4334
* Trying 0.0.0.0:2080...
* Connected to 0.0.0.0 (127.0.0.1) port 2080
> GET /pwnedsha1/03DE6C570BFE24BFC328CCD7CA46B76EADAF4334 HTTP/1.1
> Host: 0.0.0.0:2080
> User-Agent: curl/8.5.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-type: text/plain; charset=utf-8
< Accept-Ranges: bytes
< Content-Length: 6
< Date: Fri, 13 Dec 2024 09:40:19 GMT
< Server: lighttpd/1.4.76
<
51725
* Connection #0 to host 0.0.0.0 left intact
Example 2 - random password "EffAjcadigucGeys" isn't in the list (note 404 status code):
$ curl http://0.0.0.0:2080/sha1hash/EffAjcadigucGeys
76A09C6B1E7DF82E2AC8227F92F4FA7054B8DEE2
$ curl -v http://0.0.0.0:2080/pwnedsha1/76A09C6B1E7DF82E2AC8227F92F4FA7054B8DEE2
* Trying 0.0.0.0:2080...
* TCP_NODELAY set
* Connected to 0.0.0.0 (127.0.0.1) port 2080 (#0)
> GET /pwnedsha1/76A09C6B1E7DF82E2AC8227F92F4FA7054B8DEE2 HTTP/1.1
> Host: 0.0.0.0:2080
> User-Agent: curl/7.68.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 404 Not Found
< Content-type: text/plain; charset=utf-8
< Content-Length: 0
< Date: Sun, 16 Aug 2020 14:30:44 GMT
< Server: lighttpd/1.4.55
<
* Connection #0 to host 0.0.0.0 left intact
/pwnedntlm/{ntlmhash}
checks if the password with this NTLM hash has been breached.
The HTTP status code indicates whether a match is found. Possible status codes are:
- 400 Bad Request: returned if we can't find the data file, or you don't specify a hash
- 202 OK: we found the hash - the response body contains the match count
- 404 Not found: No match found in breach database
Reports on the status of the container, for healthchecks.
This software is based on the following contributions:
- Alpine Linux
- Lighttpd
- Docker image based on Sébastien Pujadas' lighttpd Docker image
- Search script based on Stefan Scherer's CLI pwned-passwords
- Uses sgrep to do the search
All content is copyrighted by the original authors.
My own contributions are free and unencumbered software released into the public domain.
This code is provided AS IS, without warranty of any kind.
See LICENSES/
for the full license text and disclaimers.
This code is updated as often as possible, but support is provided on a best effort basis only.
Please report any problems or vulnerabilities by opening a GitHub issue here.