A small, privacy-minded web utility that checks whether a password appears in the public Have I Been Pwned (HIBP) Pwned Passwords dataset using the k‑Anonymity (Range) API.
This repo is intentionally lightweight (no database, no accounts) and is designed to run locally (Windows/XAMPP or PHP built-in server) and on typical Linux hosting.
- Accepts a password in a simple web form.
- Computes its SHA‑1 hash and queries HIBP using the Range API (only the first 5 hex chars of the hash prefix are sent).
- Displays how many times the password appears in the dataset (or “not found”).
Important: this checks exposure frequency in the Pwned Passwords dataset. It does not measure general password strength.
Request flow:
- Browser submits the password to the same page (POST).
index.phpinvokescheck_password.pyand passes the password as a CLI argument.check_password.py:- SHA‑1 hashes the password (uppercase hex)
- sends only the first 5 characters to
https://api.pwnedpasswords.com/range/{prefix} - compares returned suffixes locally to find a match
- PHP returns JSON to the browser; the UI renders the returned text.
High-level diagram:
Browser
| POST / (password)
v
index.php (PHP)
| exec python check_password.py <password>
v
check_password.py (Python)
| GET https://api.pwnedpasswords.com/range/{first5}
v
HIBP Range API
This is a single-endpoint app:
GET /returns the HTML UI.POST /expectsapplication/x-www-form-urlencodedormultipart/form-datawith apasswordfield.
Response (JSON):
- Success:
{"result": "..."}(HTTP 200) - Validation:
{"error": "Password is required."}(HTTP 200) - Python execution failure:
{"error": "Error running Python script."}(HTTP 500)- When debug is enabled, the response also includes
details,exit_code, andcmd.
- When debug is enabled, the response also includes
- Frontend: HTML + CSS + vanilla JS
- Backend (web): PHP (renders page; POST handler executes Python and returns JSON)
- Backend (logic): Python 3 +
requests(calls HIBP Range API)
- PHP 8.0+ (uses
str_ends_with()) - Python 3.x
- Python package:
requests
Install Python dependency:
python -m pip install requestsFrom the repo root:
php -S 127.0.0.1:30001 -t .Then browse to:
http://127.0.0.1:30001/
Stop with Ctrl+C.
- Place the repo in your Apache document root (for XAMPP:
C:\xampp\htdocs\Password-Exposure-Checkeror similar). - Browse:
http://localhost/Password-Exposure-Checker/
If you prefer a dedicated hostname (e.g. passwordexposurechecker.localhost):
- Ensure VirtualHosts are enabled (XAMPP Apache):
Include conf/extra/httpd-vhosts.conf - Add a vhost entry similar to:
<VirtualHost *:80>
ServerName passwordexposurechecker.localhost
DocumentRoot "D:/Websites/035-2025-Password-Exposure-Checker"
<Directory "D:/Websites/035-2025-Password-Exposure-Checker">
Require all granted
AllowOverride All
Options Indexes FollowSymLinks
</Directory>
</VirtualHost>- Restart Apache.
- Browse:
http://passwordexposurechecker.localhost/
Notes:
- Many environments resolve
*.localhostto127.0.0.1automatically. If yours doesn’t, add a hosts entry mapping it to127.0.0.1.
The app is configuration-light; you can influence runtime behavior with environment variables.
Absolute path to the Python interpreter to use when executing check_password.py.
- Windows example:
C:\Python312\python.exe - Linux example:
/usr/bin/python3
Why you might need it:
- Apache service accounts often do not inherit your interactive shell
PATH. - If not set, the app tries common defaults:
- Windows:
py -3, then searches common install paths, thenpython - Linux:
/usr/bin/python3orpython3
- Windows:
Set to 1 to include Python combined output in error JSON when Python invocation fails.
Notes:
- For local requests (127.0.0.1 / ::1) debug is enabled automatically.
- Do not enable this in production.
Set to 1 to allow HTTP requests from non-local clients.
By default, index.php will enforce HTTPS when:
- the request is not local, and
- the host is not
localhost/*.localhost, and PW_ALLOW_HTTPis not1
If the app is behind a TLS-terminating reverse proxy, ensure it forwards X-Forwarded-Proto: https.
You can run the Python checker directly:
python check_password.py "correct horse battery staple"This prints a human-readable message to stdout. The PHP app returns this stdout as the result JSON field.
- The app uses HIBP k‑Anonymity: only a 5‑character SHA‑1 prefix is sent to HIBP.
- No database is used; the app does not intentionally persist submitted passwords.
- The password is posted to your server; if you expose this beyond localhost, run behind HTTPS.
- The password is passed to Python as a command-line argument.
- On some systems, process lists / logs can expose command-line arguments.
- If you intend to deploy this publicly, refactor to pass the password via stdin or a pipe instead of argv.
- The current Python output prints the plaintext password back in the response.
- Fine for a local demo; not recommended for public deployment.
.
├─ index.php # UI + POST handler; security headers; Python invocation
├─ check_password.py # HIBP Range API integration
├─ js/
│ └─ app.js # Fetches POST and renders JSON response
├─ css/
│ └─ styles.css # Styling
└─ README.md
Common causes:
- Python isn’t installed, or Apache can’t see it via
PATH. requestsisn’t installed in the Python environment being used.
Fix:
- Set
PYTHON_BINto your Python executable’s absolute path and restart Apache. - Install
requestsfor that same interpreter (python -m pip install requests).
The script calls:
https://api.pwnedpasswords.com/range/{prefix}
If you’re offline or behind a restrictive proxy/firewall, requests may fail.
If you’re testing from another device on your LAN (non-local request), index.php may redirect to HTTPS.
- Recommended: serve behind HTTPS.
- Dev-only override: set
PW_ALLOW_HTTP=1.
If you deploy publicly, consider adding:
- HTTPS end-to-end
- rate limiting / request size limits
- a refactor to avoid passing the password via argv and avoid echoing the password in responses
- No database, no user management.
- Output is a simple message string; could be extended to structured status codes.
No license file is currently included in this repository.