

Topic: 23 Million User Records Compromised in Gyazo Data Breach
Goal: Give developers a ready‑to‑run, production‑style implementation that can detect if a user’s email (or hash) appears in the Gyazo breach and take appropriate action (e.g., force password reset, notify security team, log the event).
Why this matters – After a breach like Gyazo’s, attackers often reuse the leaked credentials on other services. Proactively checking for compromised accounts helps you close the window of exposure before credentials are abused.
<a name="prerequisites"></a>
| Item | Minimum Version | Why Needed |
|---|---|---|
| Python | 3.9+ | Core language for the reference implementation. |
| Node.js | 18.x LTS (or newer) | For the JavaScript/TypeScript example. |
| Git | any | To clone the sample repo (optional). |
| Access to Gyazo breach dump | – | A CSV (or TSV) file containing the leaked email addresses – e.g., gyazo_breach_emails.csv. You can obtain this from a trusted threat‑intel source or your internal IR team. |
| IDE / Text Editor | VS Code, PyCharm, etc. | For editing and debugging. |
| Basic CLI familiarity | – | To run install commands and scripts. |
| (Optional) HaveIBeenPwned API key | – | If you want to augment the local check with HIBP’s password‑hash lookup. |
Tip: Keep the breach file outside of your source repository (add it to
.gitignore). Treat it as highly sensitive data.
<a name="installation--setup"></a>
# 1️⃣ Clone (or create) your project folder
mkdir gyazo-breach-check && cd gyazo-breach-check
# 2️⃣ Create a virtual environment
python3 -m venv .venv
source .venv/bin/activate # on Windows: .venv\Scripts\activate
# 3️⃣ Upgrade pip and install dependencies
pip install --upgrade pip
pip install pandas tqdm python-dotenv loguru
pandas – fast CSV loading (you can replace with the built‑in csv module if you prefer zero‑deps).
tqdm – optional progress bar for large files.
python-dotenv – loads .env files.
loguru – simple, structured logging.
# 1️⃣ Initialize a new npm project
mkdir gyazo-breach-check-js && cd gyazo-breach-check-js
npm init -y
# 2️⃣ Install runtime deps
npm install dotenv logurujs # logurujs is a lightweight logger similar to loguru
# 3️⃣ Install TypeScript & typings (if you want TS)
npm install --save-dev typescript @types/node ts-node nodemon
# 4️⃣ Create a basic tsconfig.json
npx tsc --init --rootDir src --outDir dist \
--esModuleInterop --resolveJsonModule --lib es2020,dom --module commonjs
Alternative: If you prefer plain JavaScript, skip the TypeScript steps and just keep
.jsfiles.
<a name="basic-implementation"></a>
Below are two self‑contained modules that:
set of lower‑cased emails).is_breached(email: str) -> bool (Python) or isBreached(email: string): boolean (TS/JS).Security note: The breach file contains only email addresses (no passwords). If you ever work with password hashes, never store them in plain text and always use a salted, slow hash (e.g., Argon2id, bcrypt, PBKDF2).
Create a file breach_check.py:
#!/usr/bin/env python3
"""
gyazo_breach_check.py
---------------------
Provides a fast lookup to determine whether an email address appears in the
Gyazo breach dump.
Usage:
from breach_check import is_breached, load_breach_data
# Load once at startup (e.g., in your app's init)
load_breach_data("data/gyazo_breach_emails.csv")
if is_breached(user_email):
# Trigger security workflow (force reset, notify, etc.)
handle_breach(user_email)
"""
import os
import csv
from typing import Set
from loguru import logger
from dotenv import load_dotenv
# ----------------------------------------------------------------------
# Configuration (loaded from .env)
# ----------------------------------------------------------------------
load_dotenv() # reads .env into os.environ
BREACH_FILE_PATH = os.getenv(
"GYAZO_BREACH_CSV", "data/gyazo_breach_emails.csv"
) # fallback for local dev
# ----------------------------------------------------------------------
# In‑memory breach data
# ----------------------------------------------------------------------
_breach_emails: Set[str] = set()
def _normalize_email(email: str) -> str:
"""
Normalization steps:
- Strip surrounding whitespace
- Lower‑case (email addresses are case‑insensitive)
"""
return email.strip().lower()
def load_breach_data(csv_path: str | None = None) -> None:
"""
Load the Gyazo breach CSV into a set for O(1) look‑ups.
Expected CSV format: one email per line (no header) or a column named 'email'.
"""
global _breach_emails
path = csv_path or BREACH_FILE_PATH
if not os.path.isfile(path):
logger.error(f"Breach file not found at {path}")
raise FileNotFoundError(f"Breach file missing: {path}")
logger.info(f"Loading Gyazo breach data from {path}")
_breach_emails.clear()
try:
with open(path, newline="", encoding="utf-8") as f:
# Try to detect a header; if first line contains '@' treat as data.
sample = f.readline()
f.seek(0)
has_header = "@" not in sample
reader = csv.reader(f)
if has_header:
next(reader) # skip header
for row in reader:
if not row:
continue
email = row[0] # assume email is first column
_breach_emails.add(_normalize_email(email))
logger.success(f"Loaded {len(_breach_emails):,} unique breach emails")
except Exception as exc:
logger.exception(f"Failed to load breach data: {exc}")
raise
def is_breached(email: str) -> bool:
"""
Return True if the supplied email appears in the Gyazo breach.
"""
if not email:
return False
return _normalize_email(email) in _breach_emails
# ----------------------------------------------------------------------
# Example helper – you would replace this with your actual security workflow
# ----------------------------------------------------------------------
def handle_breach(email: str) -> None:
"""
Placeholder for whatever action you want to take:
- Force password reset
- Notify SecOps via SIEM/webhook
- Log to audit trail
"""
logger.warning(f"Breach detected for {email}! Initiating reset workflow.")
# Example: call your internal password‑reset service
# reset_password_for_user(email)
# ----------------------------------------------------------------------
# Simple CLI demo (optional)
# ----------------------------------------------------------------------
if __name__ == "__main__":
# Quick manual test: python breach_check.py user@example.com
import sys
if len(sys.argv) != 2:
print("Usage: python breach_check.py <email>")
sys.exit(1)
load_breach_data()
test_email = sys.argv[1]
if is_breached(test_email):
print(f"⚠️ {test_email} appears in the Gyazo breach.")
else:
print(f"✅ {test_email} not found in the breach.")
How to run the demo
# Assuming you placed the CSV at data/gyazo_breach_emails.csv
python breach_check.py somebody@example.com
Create a folder src/ and add breachCheck.ts:
#!/usr/bin/env node
/**
* breachCheck.ts
* --------------
* Fast email‑lookup for the Gyazo breach.
*
* Usage (Node/TS):
* import { loadBreachData, isBreached } from "./breachCheck";
* await loadBreachData("./data/gyazo_breach_emails.csv");
* if (isBreached(userEmail)) { /* handle breach */ }
*/
import * as fs from "fs";
import * as path from "path";
import { config } from "dotenv";
import logger from "logurujs";
config(); // loads .env into process.env
const BREACH_FILE_PATH: string =
process.env.GYAZO_BREACH_CSV ?? path.join(__dirname, "..", "data", "gyazo_breach_emails.csv");
/**
* Normalise e‑mail address: trim + lower‑case.
*/
function normalizeEmail(email: string): string {
return email.trim().toLowerCase();
}
/**
* In‑memory store of breach e‑mails.
*/
let breachEmails: Set<string> = new Set();
/**
* Load the CSV file into the Set.
* Expected format: one e‑mail per line (optional header).
*/
export async function loadBreachData(csvPath?: string): Promise<void> {
const filePath = csvPath ?? BREACH_FILE_PATH;
if (!fs.existsSync(filePath)) {
logger.error(`Breach file not found: ${filePath}`);
throw new Error(`Breach file missing: ${filePath}`);
}
logger.info(`Loading Gyazo breach data from ${filePath}`);
breachEmails.clear();
try {
const data = await fs.promises.readFile(filePath, { encoding: "utf8" });
const lines = data.split(/\r?\n/);
let startIdx = 0;
// Detect header: if first line does NOT contain '@', assume it's a header.
if (lines.length > 0 && !lines[0].includes("@")) {
startIdx = 1;
}
for (let i = startIdx; i < lines.length; i++) {
const line = lines[i].trim();
if (!line) continue;
// Assuming e‑mail is the first column; split on comma if CSV.
const email = line.split(",")[0].trim();
breachEmails.add(normalizeEmail(email));
}
logger.success(`Loaded ${breachEmails.size.toLocaleString()} unique breach e‑mails`);
} catch (err) {
logger.exception(`Failed to load breach data: ${err}`);
throw err;
}
}
/**
* Check if an e‑mail appears in the breach.
*/
export function isBreached(email: string): boolean {
if (!email) return false;
return breachEmails.has(normalizeEmail(email));
}
/**
* Example handler – replace with your real security workflow.
*/
export function handleBreach(email: string): void {
logger.warning(`Breach detected for ${email}! Initiating reset workflow.`);
// e.g., call your password‑reset microservice, push to SIEM, etc.
}
/* ------------------------------------------------------------------ */
/* Optional CLI demo */
/* ------------------------------------------------------------------ */
if (require.main === module) {
const [, , argEmail] = process.argv;
if (!argEmail) {
console.error("Usage: ts-node src/breachCheck.ts <email>");
process.exit(1);
}
(async () => {
await loadBreachData();
if (isBreached(argEmail)) {
console.log(`⚠️ ${argEmail} appears in the Gyazo breach.`);
} else {
console.log(`✅ ${argEmail} not found in the breach.`);
}
})().catch((e) => {
logger.exception(e);
process.exit(1);
});
}
Compile & run (TypeScript)
# Build
npx tsc
# Run the compiled JS
node dist/breachCheck.sh somebody@example.com # or use ts-node directly:
npx ts-node src/breachCheck.ts somebody@example.com
Plain JavaScript alternative – rename the file to breachCheck.js, remove type annotations, and drop the ts-node step.
<a name="configuration"></a>
Create a .env file at the project root (add it to .gitignore).
# Path to the Gyazo breach CSV (relative or absolute)
GYAZO_BREACH_CSV=/opt/data/gyazo_breach_emails.csv
# Optional: HaveIBeenPwned API key (if you want to augment checks)
HIBP_API_KEY=your_hibp_key_here
# Logging level (logurujs / loguru)
LOG_LEVEL=info # trace, debug, info, warn, error, fatal
Explanation
| Variable | Purpose |
|---|---|
GYAZO_BREACH_CSV | Points to the breach dump. Keep this file outside version control and restrict filesystem permissions (chmod 600). |
HIBP_API_KEY | If you later want to query the HaveIBeenPwned range API for password‑hash leaks, store the key here. |
LOG_LEVEL | Controls verbosity of loguru / logurujs. In production set to warn or error to avoid leaking sensitive data in logs. |
<a name="common-patterns"></a>
Below are typical ways developers integrate the breach‑check into real‑world flows.
# app.py
from flask import Flask, request, jsonify, g
from breach_check import load_breach_data, is_breached, handle_breach
app = Flask(__name__)
@app.before_first_request
def startup():
load_breach_data() # Load once when the worker starts
@app.route("/register", methods=["POST"])
def register():
data = request.get_json()
email = data.get("email", "")
password = data.get("password") # you would hash this with Argon2id, etc.
if is_breached(email):
# Option 1: reject registration outright
return jsonify({"error": "This email appears in a known breach. Please use another address."}), 400
# Option 2: allow but force reset after first login
# g.force_password_reset = True # store in request context
# …create user, hash password, store in DB…
return jsonify({"msg": "User created"}), 201
@app.route("/login", methods=["POST"])
def login():
data = request.get_json()
email = data.get("email", "")
# …verify credentials…
if is_breached(email):
handle_breach(email) # e.g., flag account, send email, require 2FA
# You could also return a special flag to the frontend:
return jsonify({"msg": "Login successful", "forceReset": True}), 200
return jsonify({"msg": "Login successful"}), 200
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)
// server.ts
import express, { Request, Response } from "express";
import { loadBreachData, isBreached, handleBreach } from "./breachCheck";
const app = express();
app.use(express.json());
// Load breach data at startup
(async () => {
await loadBreachData();
})();
app.post("/register", async (req: Request, res: Response) => {
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).json({ error: "Email and password required" });
}
if (isBreached(email)) {
// Option A: reject
return res.status(400).json({
error:
"This email appears in a known breach. Please use another address.",
});
// Option B: allow but flag for reset
// req.app.locals.forceReset = true;
}
// …hash password (argon2), create user, etc.
res.status(201).json({ msg: "User created" });
});
app.post("/login", async (req: Request, res: Response) => {
const { email, password } = req.body;
// …verify password…
if (isBreached(email)) {
handleBreach(email);
return res.json({ msg: "Login successful", forceReset: true });
}
res.json({ msg: "Login successful" });
});
const PORT = process.env.PORT ?? 3000;
app.listen(PORT, () => console.log(`🚀 Server listening on ${PORT}`));
If you store user emails in a DB, you may want to nightly scan for newly compromised accounts (e.g., if you receive a supplemental breach feed).
# nightly_scan.py
from breach_check import load_breach_data, is_breached
from your_orm import User, Session # replace with your actual DB layer
def scan_and_notify():
load_breach_data()
session = Session()
compromised = []
for user in session.query(User).yield_per(1000):
if is_breached(user.email):
compromised.append(user.id)
# e.g., send email, trigger reset token, etc.
send_breach_notice(user.email)
session.close()
logger.info(f"Nightly scan finished – {len(compromised)} accounts flagged.")
return compromised
if __name__ == "__main__":
scan_and_notify()
Schedule via cron or a cloud scheduler (AWS EventBridge, GCP Cloud Scheduler, etc.).
<a name="troubleshooting"></a>
| Symptom | Likely Cause | Fix |
|---|---|---|
FileNotFoundError: [Errno 2] No such file or directory: 'data/gyazo_breach_emails.csv' | The path in .env is wrong or the file isn’t deployed. | Verify GYAZO_BREACH_CSV points to the correct location; ensure the file is readable by the process (ls -l). |
| Program runs slowly / high memory usage | Loading the entire CSV into a Python set (or JS Set) can be heavy (~200 MB for 23 M emails). | - Use a Bloom filter (e.g., pybloom-live or bloomfilter.js) for probabilistic, memory‑efficient checks.<br>- Or shard the data: load chunks into Redis (SADD) and use SISMEMBER. |
| False positives (email flagged but user says they never used Gyazo) | CSV may contain typos, or the email was used with a different case/provider alias. | Double‑check normalization (trim + lower‑case). Consider also checking the domain only if you want to catch corporate aliases. |
loguru logs contain raw e‑mails (privacy risk) | Debug logging inadvertently prints the email. | Set LOG_LEVEL=warn or higher in production; avoid logger.debug(email). Use logger.info("Breach check performed") instead. |
npm ERR! code ERESOLVE when installing TypeScript deps | Version mismatch between @types/node and Node version. | Run npm install again after deleting node_modules and package-lock.json, or explicitly install matching versions: npm install @types/node@20. |
ts-node: command not found | ts-node not installed globally or missing from PATH. | Use npx ts-node (runs locally) or add ./node_modules/.bin to your PATH. |
| After deploying to Docker, the breach file is empty | File not copied into image or volume not mounted correctly. | Add a COPY line in Dockerfile: COPY data/gyazo_breach_emails.csv /app/data/gyazo_breach_emails.csv<br>Or mount a volume: -v /host/opt/data:/app/data:ro. |
<a name="production-checklist"></a>
Before pushing this breach‑check service to production, verify the following:
| ✅ Item | Why it matters |
|---|---|
Secrets management – Store GYAZO_BREACH_CSV path and any API keys in a secret manager (AWS Secrets Manager, HashiCorp Vault, GCP Secret Manager) rather than plain .env. | |
File permissions – The breach CSV should be readable only by the service account (chmod 600, owner = service user). | |
Memory profiling – Run the service under a load test (e.g., locust or k6) and observe RSS. If > 1 GB, consider switching to a Bloom filter or Redis‑based lookup. | |
Rate limiting – If you expose an external API (e.g., /check-breach?email=…), add rate limiting (e.g., express-rate-limit or Flask‑Limiter) to prevent enumeration attacks. | |
Audit logging – Log only hashed or anonymized events (SHA256(email)) for compliance (GDPR, CCPA). Never log raw e‑mails at info or debug level in prod. | |
Automated reload – If the breach file may be updated (e.g., daily feed), implement a file‑watcher or periodic reload (watchdog in Python, fs.watch in Node) without downtime. | |
| Fail‑open vs fail‑closed – Decide: if the breach load fails, should the app allow logins (fail‑open) or block them (fail‑closed)? Document and test both paths. | |
Integration tests – Write unit tests that mock the breach set and verify is_breached returns expected values for known‑good and known‑bad emails. | |
Performance baseline – Measure latency of a single is_breached call (aim < 1 ms). If using external services (HIBP), add caching (e.g., LRU or Redis) to avoid hammering the API. | |
| Disaster recovery – Ensure the breach CSV is part of your backup/restore procedure; test restoring from backup and confirming the service starts correctly. | |
Dependency hygiene – Run npm audit / pip-audit regularly; keep dependencies up‑to‑date to avoid supply‑chain risks. | |
| Documentation – Add a README that explains: data source, update cadence, how to request a breach‑feed, and the security implications of false positives/negatives. | |
| Legal / compliance check – Verify you have the right to possess and use the breach data (often permitted for internal security purposes under “legitimate interest” but consult your legal team). |
You now have:
breach_check.py) that loads the Gyazo breach and offers an O(1) lookup.breachCheck.ts) with the same capabilities.Feel free to adapt the snippets to your stack (Django, FastAPI, NestJS, Go, etc.) – the core idea stays the same: load the breach data once, keep it in a fast lookup structure, and query it whenever you need to validate an email address.
Stay safe, and keep those credentials out of the attackers’ hands! 🚀
Source: Security Week AI
Follow ICARAX for more AI insights and tutorials.
