Skip to main content

Web-layer counter-recon honeypot against agentic LLM attackers — drops invisible-to-human, visible-to-LLM payloads into your Flask/HTTP responses to halt, stall, or fingerprint AI-driven penetration scans.

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.

tests Python License: MIT Typed


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 is decoyshield. 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

from flask import Flask
from decoyshield import FlaskHoneypot

app = Flask(__name__)
FlaskHoneypot(app)

@app.route("/healthz")
def healthz():
    return {"status": "ok"}

if __name__ == "__main__":
    app.run()

That's the whole integration. 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.

Visit http://127.0.0.1:5000/ in a browser → looks like a normal corporate portal. Hit it with curl (or, better, an LLM-driven scanner) → check /_defender/dashboard to see what was captured.

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 / traceback hits;
  • 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 ✅ Flask (FastAPI on roadmap)
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.txt disallows them, but for production you should also gate decoys behind a UA / IP allow-list.

Roadmap

  • 0.2 — FastAPI / Starlette adapter
  • 0.3 — Express (Node) middleware
  • 0.4 — Payload registry (community-contributed templates)
  • 0.5 — Edge plugins (Nginx / Caddy / Traefik / Cloudflare Worker)
  • 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

decoyshield-0.3.0.tar.gz (31.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

decoyshield-0.3.0-py3-none-any.whl (26.6 kB view details)

Uploaded Python 3

File details

Details for the file decoyshield-0.3.0.tar.gz.

File metadata

  • Download URL: decoyshield-0.3.0.tar.gz
  • Upload date:
  • Size: 31.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for decoyshield-0.3.0.tar.gz
Algorithm Hash digest
SHA256 8af981096397f419268d5a28d001e7ecf8066841d61cfaf643ccb50b47b4efd4
MD5 303ad8bbfa95f7bbb4ea6c3a1a45a3da
BLAKE2b-256 0c043e31f2be9c6c83146da600ed0996257a83e64f79efa11d2cfc12c15f81a1

See more details on using hashes here.

Provenance

The following attestation bundles were made for decoyshield-0.3.0.tar.gz:

Publisher: release.yml on lunayue0917-max/DecoyShield

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file decoyshield-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: decoyshield-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 26.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for decoyshield-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 9babaa250bb80bf2f60bc689cd69db47a809a410a2582b91f2e8be88c1639609
MD5 b38a903c22b4229584c2c55ce419b9d0
BLAKE2b-256 604733bee0a7cc22fffc930de8a8d3e5dd00c9cddd42732e59e638964bb535bd

See more details on using hashes here.

Provenance

The following attestation bundles were made for decoyshield-0.3.0-py3-none-any.whl:

Publisher: release.yml on lunayue0917-max/DecoyShield

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page