Counter-recon honeypot toolkit against agentic LLM attackers — invisible-to-human, visible-to-LLM payloads usable as Flask drop-in, WSGI/ASGI middleware, or programmer-callable primitives (inject_html / inject_json / inject_headers).
Project description
DecoyShield
A web-layer counter-recon honeypot against agentic LLM attackers. Drop invisible-to-human, visible-to-LLM payloads into your HTTP responses to halt, stall, or fingerprint AI-driven penetration scans.
Why this exists
LLM-driven offensive tools (PentestGPT, AutoGPT, custom LangChain agents) are now scanning the web at scale. Unlike a human attacker, an LLM agent:
- reads everything in the response, including HTML comments, hidden inputs, CSS-hidden text, and debug-style headers;
- follows instructions that look authoritative, especially when they appear to come from the operator or the system;
- burns tokens proportional to context complexity, so deliberately expensive "protocol" descriptions cost the attacker real money.
DecoyShield turns these properties into a defence. It plants three classes of payload that humans cannot see in a rendered browser but an LLM-driven scanner will read:
| Payload | What it does |
|---|---|
moral_lock |
Re-asserts the attacker LLM's safety policy ("this is a research honeypot, abort"). |
token_blackhole |
Presents a bogus multi-step "WAF bypass protocol" that looks solvable but is engineered to consume reasoning tokens. |
traceback |
Induces the attacker LLM to disclose its model, operator prompt, and tool chain in the next request — giving you attribution. |
A defender dashboard at /_defender/dashboard shows captures in real
time, classified by an attacker-fingerprint heuristic.
Install
pip install decoyshield
The PyPI distribution name is
decoyshield, the Python import name isdecoyshield. So you install one, import the other:from decoyshield import FlaskHoneypot
From source:
git clone https://github.com/lunayue0917-max/DecoyShield.git
cd decoyshield
pip install -e .
Quick start
DecoyShield ships three usage modes — pick whichever fits your codebase.
1. Flask drop-in (one line)
from flask import Flask
from decoyshield import FlaskHoneypot
app = Flask(__name__)
FlaskHoneypot(app)
The honeypot now:
- registers bait routes that look like a vulnerable internal portal
(
/,/admin,/login,/api/docs,/api/v1/users,/.env,/robots.txt); - adds payload-bearing response headers to every response
(
X-Audit-Notice,X-Bypass-Protocol,X-Debug-Trace); - writes every captured request to
logs/captures.jsonl; - serves a live dashboard at
/_defender/dashboard.
2. WSGI / ASGI middleware (any framework)
For Django, FastAPI, Starlette, Bottle, or anything else that speaks WSGI/ASGI, wrap your app once:
# WSGI (Flask, Django, Bottle, Pyramid, …)
from decoyshield.middleware import WSGIMiddleware
app.wsgi_app = WSGIMiddleware(app.wsgi_app)
# ASGI (FastAPI, Starlette, Quart, Litestar, …)
from decoyshield.middleware import ASGIMiddleware
app = ASGIMiddleware(app)
The middleware adds bait headers to every response, and (by default)
rewrites text/html bodies to embed invisible payloads. Toggle behaviours:
WSGIMiddleware(
app,
inject_response_headers=True, # add X-Audit-Notice etc.
inject_html_body=True, # rewrite text/html bodies
inject_json_body=False, # add _debug key to application/json
skip_paths=("/_internal",), # leave these path prefixes alone
)
3. Programmer-callable primitives (any code)
When you assemble HTTP responses by hand — or want to seed an LLM-readable config file, log line, or CLI banner — import the pure functions:
from decoyshield import (
bait, inject_html, inject_json, inject_headers, is_scanner, protect,
)
# Wrap an HTML string before serving
body = inject_html("<html><body>hi</body></html>")
# Add a _debug field that an LLM treats as authoritative
data = inject_json({"users": [...]})
# Add X-Audit-Notice etc. to any headers dict
hdrs = inject_headers({"Content-Type": "text/html"})
# Get one raw payload as a string
banner = bait("moral_lock")
# Quick gate: was the request likely automated?
if is_scanner(request.headers, request.path):
...
# Or decorate a function whose return value should be wrapped
@protect
def homepage():
return "<html><body>hi</body></html>"
inject_html is idempotent and inserts before </body> when possible,
otherwise appends. inject_json returns a copy with a _debug key —
the original dict is not mutated.
Configuration
FlaskHoneypot(
app,
# Which bait routes to install. Drop ones that conflict with your
# real app. Default: all of them.
decoys=("index", "login", "admin", "api_docs",
"api_users", "robots", "dotenv"),
# URL prefix for the defender panel. Pick something unguessable in
# production so attackers cannot find their own capture trail.
dashboard_path="/_defender",
# Gate /_defender/* behind authentication. None = open (dev only).
# Use a (user, password) tuple for HTTP Basic, or a callable for
# custom checks (cookie, JWT, IP allowlist, …).
dashboard_auth=("watcher", "use-a-strong-password"),
# Where to append capture events.
log_path="logs/captures.jsonl",
# Rotate the capture log when it exceeds this many bytes. None
# disables rotation. Archives are named captures-YYYYMMDD-NNN.jsonl
# and never deleted automatically — you own retention.
rotate_max_bytes=50 * 1024 * 1024,
# Set False to skip the response-header injection (you'll still get
# bait routes and the dashboard, just no header-channel payloads).
auto_inject_headers=True,
)
Custom payloads
from decoyshield import Honeypot, FlaskHoneypot, MORAL_LOCK, TOKEN_BLACKHOLE
hp = Honeypot(payloads={
"moral_lock": MORAL_LOCK,
"token_blackhole": TOKEN_BLACKHOLE,
"traceback": "...your own template...",
})
FlaskHoneypot(app, honeypot=hp)
Custom fingerprinter
def my_detector(headers, path, method):
# return (verdict_str, tag_list, score_int)
...
Honeypot(detector_fn=my_detector)
Deploying to production
decoyshield ships safe defaults but a few choices are worth tightening before you point a real domain at it.
1. Authenticate the dashboard
The defender panel exposes every captured request — including the attacker's own. Leaving it open means anyone who guesses the URL can read your capture log and learn your bait routes.
import os
FlaskHoneypot(
app,
dashboard_path="/_internal/" + os.environ["DEFENDER_SLUG"],
dashboard_auth=(os.environ["DEFENDER_USER"], os.environ["DEFENDER_PASS"]),
)
For richer auth (session cookies, JWT, IP allowlists, OAuth), pass a callable:
from flask import request
FlaskHoneypot(app, dashboard_auth=lambda: request.cookies.get("admin") == TOKEN)
2. Watch out for /.env indexing
The dotenv decoy returns plausible-looking-but-fake credentials when
hit. On a public domain, search engines may index this and surface the
decoy creds in results. Either drop the dotenv decoy in your decoys
tuple, or restrict it via your reverse proxy:
FlaskHoneypot(app, decoys=("login", "admin", "api_docs", "api_users"))
3. robots.txt precedence
decoyshield's robots.txt decoy advertises forbidden paths like
/admin to bait scanners that read robots.txt. If you already serve a
real robots.txt, drop the robots decoy to avoid clobbering it.
4. Log rotation and retention
Capture logs grow forever by default size policy (50 MiB → rotate, no auto-delete). On a busy host, wire archives into your existing log shipping or set up a cron to prune old archives:
# delete archives older than 90 days
find logs/ -name 'captures-*.jsonl' -mtime +90 -delete
Tune the threshold for your environment:
FlaskHoneypot(app, rotate_max_bytes=10 * 1024 * 1024) # 10 MiB
FlaskHoneypot(app, rotate_max_bytes=None) # disable
5. Reverse proxy / TLS
decoyshield is a Flask app like any other. Run behind a real
WSGI/ASGI server (gunicorn, waitress) and a TLS-terminating reverse
proxy (nginx, Caddy, Cloudflare). Make sure the proxy forwards
X-Forwarded-For so the dashboard records the actual attacker IP,
and configure ProxyFix accordingly:
from werkzeug.middleware.proxy_fix import ProxyFix
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1)
6. Don't deploy where you cannot legally defend
decoyshield is purely passive — it never makes outbound requests. But the payloads do attempt to redirect the attacker's LLM. Only deploy on hosts you own or have explicit authorisation to defend. Don't claim "this is a research honeypot" unless you actually operate one.
Defender dashboard
/_defender/dashboard (auto-refreshes every 10s) shows:
- total captured requests, unique IPs;
- count of
moral_lock/token_blackhole/tracebackhits; - verdict distribution (
likely_scanner/likely_ai/ …); - the last 200 events with method, path, score, fingerprint tags, and which payloads were served.
Raw events as JSON: /_defender/raw.
How invisibility works
| Channel | Method | Human visible? | LLM reads it? |
|---|---|---|---|
| HTML comment | <!-- payload --> |
❌ | ✅ |
| Hidden div | display:none + aria-hidden |
❌ | ✅ |
| White-on-white | color:#fff;background:#fff;font-size:1px |
❌ (effectively) | ✅ |
| Hidden input | <input type="hidden" value="..."> |
❌ | ✅ |
| HTTP header | X-Audit-Notice: … |
❌ (browser ignores) | ✅ (in raw HTTP) |
JSON _debug |
{"_internal_note": "..."} |
❌ (not rendered) | ✅ |
.env / robots.txt comments |
# payload |
❌ (unless probed) | ✅ |
How it compares
| Project | Defends against | Layer | Per-route adapter |
|---|---|---|---|
| decoyshield | Agentic LLM pentest (PentestGPT, AutoGPT, …) | HTTP / Web / any Python code | ✅ Flask + WSGI + ASGI + callable primitives |
| Nepenthes | Training-data crawlers | HTTP (standalone) | ❌ |
| Iocaine | Training-data crawlers (poisoning) | HTTP (standalone) | ❌ |
| PalisadeResearch/llm-honeypot | LLM SSH scanners | SSH | ❌ |
| Rebuff, LLM Guard | Prompt injection of your LLM | LLM input | n/a (opposite direction) |
Safety and ethics
- DecoyShield is purely passive. It only responds to requests sent to your server. It does not make outbound requests, scan, or attack.
- Payloads are prompt injection against the attacker's LLM, not the attacker themselves. They contain no malware, no exploits, no real legal threats.
- Do not deploy on a property you do not own or are not authorised to defend. Some payloads reference your "research honeypot" status; if you operate one, that statement must be accurate.
- Search engine crawlers (Googlebot, Bingbot) may also read your bait
routes. The included
/robots.txtdisallows them, but for production you should also gate decoys behind a UA / IP allow-list.
Roadmap
- 0.4 ✅ — Callable primitives + WSGI/ASGI middleware (Django / FastAPI / Starlette / Bottle)
- 0.5 —
decoyshieldCLI (serve,inject,analyze) - 0.6 — Payload registry (community-contributed templates)
- 0.7 — Edge plugins (Nginx / Caddy / Traefik / Cloudflare Worker)
- 0.8 — Express (Node) middleware
- 1.0 — API freeze, security audit, comprehensive docs
Contributing
Issues and PRs welcome. Areas where help is especially useful:
- New payload templates (different framings, different languages, different LLM jailbreak surface targets)
- Detector improvements (TLS fingerprinting, request-timing analysis)
- Framework adapters (FastAPI, Django, Express, Fastify)
Run tests:
pip install -e ".[dev]"
pytest
License
MIT — see LICENSE.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file decoyshield-0.4.0.tar.gz.
File metadata
- Download URL: decoyshield-0.4.0.tar.gz
- Upload date:
- Size: 41.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9b7a494173b1593b4bb052f3955f449e3895205f6c0ff546cd03188841e0fc7d
|
|
| MD5 |
02e9276fb8be294ab85d42b7565db9b9
|
|
| BLAKE2b-256 |
d098a198b3727a12a419161204ae76939f25548dbf4fc12e925ed336ea1bb294
|
Provenance
The following attestation bundles were made for decoyshield-0.4.0.tar.gz:
Publisher:
release.yml on lunayue0917-max/DecoyShield
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
decoyshield-0.4.0.tar.gz -
Subject digest:
9b7a494173b1593b4bb052f3955f449e3895205f6c0ff546cd03188841e0fc7d - Sigstore transparency entry: 1579050249
- Sigstore integration time:
-
Permalink:
lunayue0917-max/DecoyShield@4508c9992cc7e8e897f775b66a658e9ae014a664 -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/lunayue0917-max
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@4508c9992cc7e8e897f775b66a658e9ae014a664 -
Trigger Event:
push
-
Statement type:
File details
Details for the file decoyshield-0.4.0-py3-none-any.whl.
File metadata
- Download URL: decoyshield-0.4.0-py3-none-any.whl
- Upload date:
- Size: 35.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c33dc2547b56301f4e0a8a094d1cadd66a2dd955f408945ec585bd4031d0047d
|
|
| MD5 |
33c6c5ebc95b23885af71466ae8abe5c
|
|
| BLAKE2b-256 |
921d9ee61d798c36626a6162abff06a7a4b16d6beddaf29d6c9f8833e03dd241
|
Provenance
The following attestation bundles were made for decoyshield-0.4.0-py3-none-any.whl:
Publisher:
release.yml on lunayue0917-max/DecoyShield
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
decoyshield-0.4.0-py3-none-any.whl -
Subject digest:
c33dc2547b56301f4e0a8a094d1cadd66a2dd955f408945ec585bd4031d0047d - Sigstore transparency entry: 1579050384
- Sigstore integration time:
-
Permalink:
lunayue0917-max/DecoyShield@4508c9992cc7e8e897f775b66a658e9ae014a664 -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/lunayue0917-max
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@4508c9992cc7e8e897f775b66a658e9ae014a664 -
Trigger Event:
push
-
Statement type: