

ICARAX Tech Blog – Security & AI
Context: A dark‑web marketplace recently advertised 153 million driver‑license photos. If any of those images appear in your application (e.g., user‑profile uploads, KYC flows, document‑verification pipelines) you need a fast, automated way to flag them before they are processed or stored.
The guide below shows how to build a light‑weight, production‑ready detector that:
LEAKED, SUSPICIOUS, CLEAN).The implementation is provided for Python (Flask) and JavaScript/TypeScript (Node/Express). Both versions share the same logic; you can pick the stack that matches your service.
| Item | Why you need it | How to obtain |
|---|---|---|
| Python 3.9+ (or Node 18+) | Runtime for the code samples | https://www.python.org/downloads/ / https://nodejs.org/ |
| Git | Clone the example repo (optional) | https://git-scm.com/ |
| Tesseract OCR | Open‑source OCR engine used by pytesseract / tesseract.js | https://github.com/tesseract-ocr/tesseract (install via OS package manager) |
| Internet access (optional) | To download a sample leaked‑hash list or to call cloud vision APIs if you prefer | – |
| API keys (optional) | If you decide to use a commercial OCR/face service (AWS Rekognition, Azure Vision, Google Cloud Vision) instead of the open‑source stack | Sign up for the respective cloud console and create a key |
| Basic CLI familiarity | To run install commands and start the servers | – |
Tip: The open‑source stack (Tesseract + a lightweight face detector) works offline and incurs no per‑call cost, making it ideal for bulk scanning or edge deployments. Swap in a cloud API later if you need higher accuracy.
# 1️⃣ Clone the repo (or create a fresh folder)
git clone https://github.com/icarax/dl-leak-detector-py.git
cd dl-leak-detector-py
# 2️⃣ Create a virtual environment (recommended)
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
# 3️⃣ Install dependencies
pip install --upgrade pip
pip install flask pillow pytesseract deepface python-dotenv
# 4️⃣ (Optional) Install Tesseract system binary
# Ubuntu/Debian:
sudo apt-get update && sudo apt-get install -y tesseract-ocr
# macOS (brew):
brew install tesseract
# Windows: download installer from https://github.com/UB-Mannheim/tesseract/wiki
# 1️⃣ Clone the repo (or create a fresh folder)
git clone https://github.com/icarax/dl-leak-detector-js.git
cd dl-leak-detector-js
# 2️⃣ Initialise npm project (if not already)
npm init -y
# 3️⃣ Install dependencies
npm install express sharp tesseract.js face-api.js dotenv
# 4️⃣ Install Tesseract system binary (same as Python step)
# Ubuntu/Debian:
sudo apt-get update && sudo apt-get install -y tesseract-ocr
# macOS (brew):
brew install tesseract
# Windows: see above
Note:
face-api.jsrequires a few model files (downloaded automatically on first run). The first request may take a couple of seconds while the models are fetched from CDN and cached locally.
Below are complete, copy‑paste‑ready mini‑services. They expose a single POST /scan endpoint that accepts an image (multipart/form-data, field name image) and returns JSON with the risk assessment.
app.py#!/usr/bin/env python3
"""
dl-leak-detector-py/app.py
Flask micro‑service that scores uploaded images for possible driver‑license leakage.
"""
import os
import hashlib
import re
from io import BytesIO
from typing import Tuple
import numpy as np
from PIL import Image
from flask import Flask, request, jsonify
from dotenv import load_dotenv
import pytesseract
import cv2 # OpenCV for face detection (haarcascade)
# ----------------------------------------------------------------------
# Load configuration from .env
# ----------------------------------------------------------------------
load_dotenv() # reads .env into os.environ
LEAKED_HASHES_PATH = os.getenv("LEAKED_HASHES_PATH", "leaked_hashes.txt")
OCR_LANG = os.getenv("OCR_LANG", "eng")
FACE_CASCADE_PATH = os.getenv(
"FACE_CASCADE_PATH",
cv2.data.haarcascades + "haarcascade_frontalface_default.xml",
)
# ----------------------------------------------------------------------
# Helper: load known leaked SHA‑256 hashes (one per line)
# ----------------------------------------------------------------------
def load_leaked_hashes() -> set:
if not os.path.isfile(LEAKED_HASHES_PATH):
# If the file is missing we start with an empty set – you can
# populate it later via a secure ingest pipeline.
return set()
with open(LEAKED_HASHES_PATH, "r", encoding="utf-8") as f:
return {line.strip().lower() for line in f if line.strip()}
LEAKED_HASHES = load_leaked_hashes()
# ----------------------------------------------------------------------
# Helper: compute SHA‑256 of image bytes
# ----------------------------------------------------------------------
def sha256_of_bytes(data: bytes) -> str:
return hashlib.sha256(data).hexdigest()
# ----------------------------------------------------------------------
# Helper: OCR → extract text, then look for driver‑license patterns
# ----------------------------------------------------------------------
DL_PATTERNS = [
# Example: US‑style: 1-8 digits, sometimes with letters (state dependent)
r"\b[A-Z]{1,2}\d{4,8}\b", # e.g., "A1234567"
r"\b\d{1,8}[A-Z]{1,2}\d{2,4}\b", # e.g., "123456AB"
# Expiry date patterns (MM/YY or MM/YYYY)
r"\b(0[1-9]|1[0-2])/\d{2,4}\b",
]
def text_looks_like_dl(text: str) -> bool:
"""Return True if any driver‑license‑ish pattern is found."""
upper = text.upper()
for pat in DL_PATTERNS:
if re.search(pat, upper):
return True
return False
# ----------------------------------------------------------------------
# Helper: simple face detection using Haar cascades (fast, no DL needed)
# ----------------------------------------------------------------------
face_cascade = cv2.CascadeClassifier(FACE_CASCADE_PATH)
def has_face(image: Image.Image) -> bool:
"""Detect at least one frontal face in a PIL image."""
# Convert PIL → OpenCV (BGR)
cv_img = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR)
gray = cv2.cvtColor(cv_img, cv2.COLOR_BGR2GRAY)
faces = face_cascade.detectMultiScale(
gray, scaleFactor=1.1, minNeighbors=5, minSize=(30, 30)
)
return len(faces) > 0
# ----------------------------------------------------------------------
# Flask app
# ----------------------------------------------------------------------
app = Flask(__name__)
@app.route("/scan", methods=["POST"])
def scan():
"""
Expects: multipart/form-data with field `image`.
Returns JSON:
{
"hash": "<sha256>",
"leaked_match": bool,
"ocr_text": "<extracted text>",
"dl_pattern_found": bool,
"face_detected": bool,
"risk": "CLEAN|SUSPICIOUS|LEAKED",
"score": 0.0-1.0
}
"""
if "image" not in request.files:
return jsonify({"error": "No image file provided (field name must be 'image')"}), 400
file = request.files["image"]
raw_bytes = file.read()
# 1️⃣ Hash check
img_hash = sha256_of_bytes(raw_bytes)
leaked_match = img_hash in LEAKED_HASHES
# 2️⃣ Load image for OCR / face detection
try:
pil_img = Image.open(BytesIO(raw_bytes)).convert("RGB")
except Exception as exc:
return jsonify({"error": f"Unable to open image: {exc}"}), 400
# 3️⃣ OCR (Tesseract)
try:
ocr_text = pytesseract.image_to_string(pil_img, lang=OCR_LANG)
except Exception as exc:
# If Tesseract isn't installed or not in PATH, we fail gracefully.
return jsonify({"error": f"OCR failed: {exc}"}), 500
dl_pattern = text_looks_like_dl(ocr_text)
# 4️⃣ Face detection
face_detected = has_face(pil_img)
# 5️⃣ Risk scoring (simple heuristic)
score = 0.0
if leaked_match:
score += 0.6 # strong signal
if dl_pattern:
score += 0.2
if face_detected:
score += 0.1
# Clamp
score = min(score, 1.0)
if score >= 0.6:
risk = "LEAKED"
elif score >= 0.3:
risk = "SUSPICIOUS"
else:
risk = "CLEAN"
return jsonify(
{
"hash": img_hash,
"leaked_match": leaked_match,
"ocr_text": ocr_text.strip(),
"dl_pattern_found": dl_pattern,
"face_detected": face_detected,
"risk": risk,
"score": round(score, 3),
}
)
if __name__ == "__main__":
# For production use a proper WSGI server (gunicorn, uWSGI, etc.)
app.run(host="0.0.0.0", port=int(os.getenv("PORT", 5000)), debug=False)
| Step | Explanation |
|---|---|
| Load leaked hashes | Reads a newline‑separated file of SHA‑256 hashes (you can generate this list from the dark‑web dump). |
| Hash check | Exact match → high confidence leak. |
| OCR | Uses Tesseract to extract any printed text. |
| Pattern matching | Simple regexes that capture common driver‑license formats (feel free to extend for your jurisdiction). |
| Face detection | Haar‑cascade frontal face detector (fast, no GPU needed). |
| Risk score | Weighted sum: leaked hash (0.6), DL pattern (0.2), face (0.1). Adjust thresholds as needed. |
| Response | Returns all intermediate data plus a final risk label. |
src/server.ts/**
* dl-leak-detector-js/src/server.ts
* Express micro‑service (Node.js) that scores uploaded images for possible driver‑license leakage.
*/
import express, { Request, Response } from "express";
import dotenv from "dotenv";
import path from "path";
import { createHash } from "crypto";
import sharp from "sharp";
import { TesseractWorker } from "tesseract.js";
import { loadFaceDetectionNet, detectSingleFace } from "face-api.js";
import fs from "fs";
dotenv.config();
const app = express();
const PORT = Number(process.env.PORT) || 3000;
// ----------------------------------------------------------------------
// Configuration
// ----------------------------------------------------------------------
const LEAKED_HASHES_PATH = process.env.LEAKED_HASHES_PATH ?? path.join(__dirname, "..", "leaked_hashes.txt");
const OCR_LANG = process.env.OCR_LANG ?? "eng";
// Load leaked hashes once at startup
const leakedHashes: Set<string> = new Set(
fs.existsSync(LEAKED_HASHES_PATH)
? fs.readFileSync(LEAKED_HASHES_PATH, "utf8")
.split("\n")
.map((l) => l.trim().toLowerCase())
.filter(Boolean)
: []
);
// ----------------------------------------------------------------------
// Helper: SHA‑256 of a Buffer
// ----------------------------------------------------------------------
function sha256(buffer: Buffer): string {
return createHash("sha256").update(buffer).digest("hex");
}
// ----------------------------------------------------------------------
// Helper: OCR via tesseract.js
// ----------------------------------------------------------------------
const ocrWorker = new TesseractWorker();
await ocrWorker.load();
await ocrWorker.loadLanguage(OCR_LANG);
await ocrWorker.initialize(OCR_LANG);
// ----------------------------------------------------------------------
// Helper: Driver‑license pattern detection (same regexes as Python)
// ----------------------------------------------------------------------
const DL_PATTERNS = [
/\b[A-Z]{1,2}\d{4,8}\b/g,
/\b\d{1,8}[A-Z]{1,2}\d{2,4}\b/g,
/\b(0[1-9]|1[0-2])\/\d{2,4}\b/g,
];
function looksLikeDL(text: string): boolean {
const upper = text.toUpperCase();
return DL_PATTERNS.some((re) => re.test(upper));
}
// ----------------------------------------------------------------------
// Helper: Face detection using face-api.js (SSD MobilenetV1)
// ----------------------------------------------------------------------
await loadFaceDetectionNet(
path.join(__dirname, "..", "models")
); // ensure you have the model files downloaded (see README)
async function hasFace(buffer: Buffer): Promise<boolean> {
const img = await sharp(buffer).raw().toBuffer({ resolveWithObject: true });
const { data, info } = img;
const detection = await detectSingleFace(
new Float32Array(data),
info.width,
info.height,
info.channels
);
return detection !== null;
}
// ----------------------------------------------------------------------
// Middleware: multipart/form-data (using express's built-in parser)
// ----------------------------------------------------------------------
app.use(express.urlencoded({ extended: true, limit: "10mb" }));
app.use(express.json({ limit: "10mb" }));
app.use(express.static("public")); // optional static folder
// ----------------------------------------------------------------------
// Route: POST /scan
// ----------------------------------------------------------------------
app.post("/scan", async (req: Request, res: Response) => {
// Expect multipart/form-data with field `image`
if (!req.files || !req.files.image) {
return res.status(400).json({ error: "No image file supplied (field name must be 'image')" });
}
const file = req.files.image as express.FileArray[0]; // adjust if using multer etc.
// For simplicity we assume raw buffer; if using multer, file.buffer exists.
const rawBuf: Buffer = file.buffer ?? (await file.arrayBuffer());
// 1️⃣ Hash check
const hash = sha256(rawBuf);
const leakedMatch = leakedHashes.has(hash.toLowerCase());
// 2️⃣ OCR
const { text: ocrText } = await ocrWorker.recognize(rawBuf, OCR_LANG);
const dlPattern = looksLikeDL(ocrText);
// 3️⃣ Face detection
const faceDetected = await hasFace(rawBuf);
// 4️⃣ Risk scoring (same weights as Python)
let score = 0.0;
if (leakedMatch) score += 0.6;
if (dlPattern) score += 0.2;
if (faceDetected) score += 0.1;
score = Math.min(score, 1.0);
const risk =
score >= 0.6 ? "LEAKED" : score >= 0.3 ? "SUSPICIOUS" : "CLEAN";
res.json({
hash,
leakedMatch,
ocrText: ocrText.trim(),
dlPatternFound: dlPattern,
faceDetected,
risk,
score: Number(score.toFixed(3)),
});
});
// ----------------------------------------------------------------------
// Start server
// ----------------------------------------------------------------------
app.listen(PORT, () => {
console.log(`🚀 Server listening on http://localhost:${PORT}`);
});
Explanation of key parts
- Leaked hash list – Loaded once at startup; you can refresh it via a background job or a secure API.
- OCR –
tesseract.jsruns in the Node process; the first call loads the language data (takes ~1 s).- Face detection – Uses
face-api.jswith the SSD MobilenetV1 model (approx. 5 MB). Place the model files (ssd_mobilenetv1_model-weights_manifest.json, etc.) in amodels/folder adjacent to the script.- Multipart handling – For brevity the example assumes you’re using a middleware like
multerthat puts the file inreq.files. Replace with your preferred parser if needed.
Both services return the same JSON shape, making it easy to swap the backend without changing client code.
Create a .env file in the project root (same level as app.py or src/server.ts). Example contents:
# -------------------------------------------------
# Core settings
# -------------------------------------------------
PORT=5000 # Python Flask port (or 3000 for Node)
LEAKED_HASHES_PATH=./leaked_hashes.txt # One SHA‑256 hash per line, lower‑case
OCR_LANG=eng # Tesseract language pack (eng, spa, fra, etc.)
# -------------------------------------------------
# Optional: If you prefer a cloud OCR/face service
# -------------------------------------------------
# USE_CLOUD_OCR=true
# AWS_ACCESS_KEY_ID=YOUR_AWS_KEY
# AWS_SECRET_ACCESS_KEY=YOUR_AWS_SECRET
# AWS_REGION=us-east-1
# # OR
# AZURE_COMPUTER_VISION_ENDPOINT=https://<your-resource>.cognitiveservices.azure.com/
# AZURE_COMPUTER_VISION_KEY=YOUR_AZURE_KEY
leaked_hashes.txtIf you have obtained a list of SHA‑256 hashes of the leaked driver‑license images (e.g., from a threat‑intel feed), place one hash per line:
a3f1c2e8b7d4f6a9b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5
...
Security note: Treat this file as sensitive – it contains identifiers of illicit content. Store it outside the web root, restrict file permissions (
chmod 600), and consider encrypting it at rest with a key managed by a secrets manager (AWS KMS, HashiCorp Vault, etc.).
| Pattern | Python (Flask) | JavaScript/Node | Why it’s useful |
|---|---|---|---|
| Factory‑style service initialization | load_leaked_hashes() called at module import | leakedHashes constant loaded once | Avoids re‑reading the hash list on every request. |
| Dependency injection for OCR/face | Global pytesseract & cv2.CascadeClassifier | Global ocrWorker & loadFaceDetectionNet | Prevents costly re‑initialization per request. |
| Centralized error handling | @app.errorhandler(500) (optional) | app.use((err, req, res, next) => { … }) | Guarantees consistent JSON error responses. |
| Request size limit | app.config['MAX_CONTENT_LENGTH'] = 10 * 1024 * 1024 | express.json({ limit: "10mb" }) | Stops DoS via huge uploads. |
| Health check endpoint | @app.route("/health") returning {"status":"ok" | app.get("/health", …) | Allows orchestrators (K8s, Docker Swarm) to verify liveness. |
| Logging | Use logging module with JSON formatter | Use pino or winston with JSON output | Structured logs simplify SIEM ingestion. |
| Rate limiting | Flask-Limiter (@limiter.limit("10/minute")) | express-rate-limit | Throttles abusive clients. |
| Graceful shutdown | signal.signal(signal.SIGTERM, …) | process.on('SIGTERM', …) | Allows in‑flight requests to finish before pod termination. |
| Symptom | Likely Cause | Fix |
|---|---|---|
TesseractNotFoundError (Python) or Error: Failed to load the tesseract.js worker (Node) | Tesseract OCR binary not installed or not in $PATH. | Install Tesseract system package (apt-get install tesseract-ocr, brew install tesseract, or Windows installer). Verify with tesseract --version. |
cv2.error: OpenCV(4.8.0) … when loading Haar cascade | FACE_CASCADE_PATH points to a non‑existent file. | Ensure OpenCV is installed (pip install opencv-python) and the path is correct; default uses OpenCV’s built‑in data directory. |
Error: Cannot find module 'sharp' | Native dependencies not built (common on Apple M1 or Windows). | Run npm install --sharp-dist-base=https://install.luke sharp or ensure you have Python/build tools installed (npm install --global windows-build-tools on Windows). |
Service returns 500 with OCR failed: ... | OCR language data missing (eng.traineddata). | Install language packs: sudo apt-get install tesseract-ocr-eng (Linux) or download from https://github.com/tesseract-ocr/tessdata. |
Hash check always false even though you know the image is leaked | Hash list file not loaded or contains extra whitespace/newlines. | Verify leaked_hashes.txt contains lower‑case hashes, trimmed. Print a few hashes on startup to debug. |
| High latency (>2 s) per request | Face‑detector model loading each request (Node version). | Move loadFaceDetectionNet and model loading outside the request handler (as shown). |
| Docker container fails to start | Missing system libraries (libtesseract, libpng, etc.). | Use a base image that includes them, e.g., python:3.11-slim + apt-get update && apt-get install -y tesseract-ocr libglib2.0-0. |
| Memory OOM on large uploads | Image not downscaled before OCR/face detection. | Add a preprocessing step: sharp(buffer).resize({ width: 800 }).toBuffer() to cap size while preserving readability. |
Before exposing the service to the public or internal users, run through this checklist:
| ✅ Item | Why it matters | How to verify |
|---|---|---|
| Transport security | Prevent eavesdropping on image uploads. | Deploy behind TLS (HTTPS). Use Let’s Encrypt or your cloud LB. |
| Input validation | Block malicious files (e.g., scripts, oversized uploads). | Verify Content-Type starts with image/; enforce size limit; optionally run file magic‑number check. |
| Sanitize file names | Avoid path traversal or overwriting critical files. | Never store the original filename; use a UUID or hash as the storage key. |
| Secrets management | API keys, hash list, and cloud credentials must not be in source. | Use .env excluded from VCS, or a secret manager (AWS Secrets Manager, GCP Secret Manager, Vault). |
| Immutable hash list | Prevent tampering that would let leaked images slip through. | Store the hash list in a read‑only volume or signed object (e.g., S3 with versioning + signature verification). |
| Rate limiting & throttling | Stop abuse or credential‑stuffing attempts. | Deploy express-rate-limit / Flask-Limiter; set sensible limits (e.g., 20 req/min per IP). |
| Observability | Detect anomalies, track false positives/negatives. | Emit structured logs (requestId, hash, risk, latency). Export metrics (Prometheus) for request_count, risk_distribution. |
| Automatic hash‑list updates | New leaks appear constantly. | Set up a cron job or serverless function that pulls the latest threat‑intel feed, verifies signatures, and replaces leaked_hashes.txt atomically. |
| Fail‑soft behavior | If OCR or face detection is temporarily unavailable, still return a useful result. | Wrap external calls in try/catch; if they fail, fall back to hash‑only scoring and set risk to UNKNOWN. |
| Regular security scanning | Ensure no known vulnerabilities in dependencies. | Run npm audit / pip check and snyk test or dependabot alerts. |
| Load testing | Confirm the service can handle expected traffic spikes. | Use k6, locust, or hey to simulate concurrent uploads; monitor CPU/memory/latency. |
| Legal & compliance | Storing or processing PII (driver‑license data) may be regulated. | Ensure you have a data‑processing agreement, retain only what’s necessary, and delete images after scoring unless retention is required by law. |
| Versioned deployments | Ability to rollback if a new model introduces regressions. | Use Docker tags or semantic versioning; keep previous image in registry. |
| Documentation & runbooks | Operators need to know how to troubleshoot and update. | Keep a README.md with deployment steps, env var reference, and incident response guide. |
If you just want to spin up a demo locally, the following Bash snippet works for both stacks (choose the language you prefer):
# ---- Python Demo ----
git clone https://github.com/icarax/dl-leak-detector-py.git
cd dl-leak-detector-py
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt # (flask, pillow, pytesseract, opencv-python, python-dotenv)
echo "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" > leaked_hashes.txt # example hash of empty file
python app.py
# Then: curl -X POST -F "image=@sample.jpg" http://localhost:5000/scan
# ---- Node Demo ----
git clone https://github.com/icarax/dl-leak-detector-js.git
cd dl-leak-detector-js
npm install
echo "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" > leaked_hashes.txt
npm start # runs ts-node src/server.ts (ensure ts-node is installed globally or via npx)
# Then: curl -X POST -F "image=@sample.jpg" http://localhost:3000/scan
Replace sample.jpg with any JPEG/PNG you want to test. If the hash matches the one you placed in leaked_hashes.txt, you’ll see "leaked_match": true and a "risk": "LEAKED" response.
You now have a complete, end‑to‑end example that:
Feel free to extend the regex list for your country's license format, swap in a cloud OCR/face API for higher accuracy, or integrate the scoring function into a larger KYC/fraud‑prevention pipeline.
Stay safe, and happy coding! 🚀
Source: Security Week AI
Follow ICARAX for more AI insights and tutorials.
