Skip to main content

flask-NoBot

Lightweight, zero-dependency bot protection for Flask. Invisible JS challenge (no CAPTCHA), regex/CIDR rule engine, HMAC-signed verification tokens.

PyPI Python License Issues Stars Downloads

Install

pip install flask-nobot

Quick start

from flask import Flask
from flask_nobot import NoBot

app = Flask(__name__)
nobot = NoBot(app, secret="change-me")

App factory

nobot = NoBot()

def create_app():
    app = Flask(__name__)
    app.config["SECRET_KEY"] = "change-me"
    nobot.init_app(app)
    return app

Secret resolution: constructor arg → NOBOT_SECRET → SECRET_KEY.

Config (app.config)

Key Default Meaning
NOBOT_SECRET — HMAC key (falls back to SECRET_KEY)
NOBOT_MODE auto auto (rules only), all (challenge everything), off
NOBOT_THRESHOLD 10 Weight total → challenge
NOBOT_TOKEN_TTL 3600 Verification cookie lifetime (s)
NOBOT_NONCE_TTL 90 Challenge-page nonce lifetime (s)
NOBOT_TRUST_PROXY False Honor X-Forwarded-For
NOBOT_DENY_BOGONS False Deny bogon/private source IPs (enable behind trusted proxy)
NOBOT_CRAWLER_WEIGHT 4 Score added when UA looks like a crawler
NOBOT_RULES [] Extra Rule objects

Change prefix via NoBot(prefix="SEC_").

Route decorators

from flask_nobot import skip, protect, challenge, block

@app.route("/health")
@skip
def health(): ...          # never challenged

@app.route("/login")
@protect
def login(): ...           # always evaluated

@app.route("/admin")
@challenge
def admin(): ...           # always challenged

@app.route("/api")
@block
def api(): ...             # failed challenge → 403, no retry
Decorator Effect
skip Bypass middleware
protect Force rule eval even in auto mode
challenge Always issue challenge
block Deny outright on failed challenge

Rules

from flask_nobot import Rule

rules = [
    Rule("trusted-office", action="allow",
         remote_addresses=["10.0.0.0/8", "192.168.1.0/24"]),
    Rule("known-bad", action="deny",
         user_agent=r"(curl|wget|python-requests)"),
    Rule("suspicious-ua", action="weigh", weight=6,
         user_agent=r"(bot|crawler|spider|scrap)"),
    Rule("no-accept-lang", action="weigh", weight=4,
         headers={"Accept-Language": r"^$"}),
    Rule("admin-paths", action="challenge",
         path=r"^/admin", method="GET|POST"),
]

nobot = NoBot(app, secret="...", rules=rules, threshold=8)

Built-in preset

DEFAULT_RULES is applied when no rules argument is given. Pass rules=[] to disable, or extend it:

from flask_nobot import NoBot, DEFAULT_RULES, Rule

nobot = NoBot(app, secret="...")  # uses DEFAULT_RULES
nobot = NoBot(app, secret="...", rules=DEFAULT_RULES + [Rule(...)])

DEFAULT_RULES covers: Cloudflare-Worker deny, known-bad/vuln/WP scanners, dotfile/shell/traversal probes (deny); well-known, favicon, robots, sitemap, health, search engines, feed readers, monitoring, link previews, archive.org (allow); AI bots, headless browsers, aggressive scrapers, empty UA (challenge); curl/wget, missing Accept/Accept-Language, Connection:close (weigh).

Rule fields

Field Type Notes
name str identifier
action allow / deny / challenge / weigh
path regex matches URL path
method regex HTTP method (fullmatch, case-insensitive)
user_agent regex
headers dict[name, regex] header must be present AND match regex
missing_headers list[name] all listed headers must be absent/empty
remote_addresses list[cidr] any match
weight int added to score on weigh

Evaluation: first matching allow/deny/challenge wins → short-circuit. All matching weigh rules accumulate → if score ≥ threshold → challenge.

Challenge

Invisible, no user input. Serves a dark HTML page that runs JS signal collection:

  • navigator.webdriver, plugin/language counts, hardware concurrency
  • Headless/automation markers: _phantom, __nightmare, $cdc_*, webdriver attr
  • Native Function.toString integrity
  • WebGL vendor/renderer (blocks SwiftShader/llvmpipe)
  • Notification-permission inconsistency trick
  • Chrome object presence vs UA
  • Timing floor/ceiling

Posts signals → server-side scoring → HMAC token cookie → redirect to original URL.

Verification token

base64(payload).base64(hmac_sha256(secret, payload)) where payload binds {timestamp, ip-hash, ua-hash, random}. IP/UA rebinding prevents cookie theft; TTL limits replay.

Security

  • HttpOnly, Secure (when HTTPS), SameSite=Strict cookies
  • Required Origin + X-Requested-With check on verify → CSRF defense
  • CSP on challenge page (default-src 'none', no third-party), X-Frame-Options: DENY, Referrer-Policy: strict-origin-when-cross-origin, X-Content-Type-Options: nosniff
  • Constant-time HMAC compare
  • One-time nonce: each challenge nonce's random r is consumed server-side on first verify → replay blocked for nonce_ttl
  • Crawler heuristic (is_crawler): regex over UA for bot signals, known tools, URLs-in-UA, absence of real browser tokens → adds crawler_weight to score
  • Bogon detection (is_bogon): blocks RFC1918/loopback/link-local/reserved ranges when deny_bogons=True (enable behind trusted proxy)

Formatting

pip install black isort
isort . && black .
npx prtfm

License

Apache-2.0

Metadata

Release files for flask-nobot 1.0.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for flask-nobot 1.0.1
File Size Uploaded
flask_nobot-1.0.1.tar.gz 21.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for flask-nobot 1.0.1
File Interpreter ABI Platform
flask_nobot-1.0.1-py3-none-any.whl Python 3 none any Details

Total release size: 40.3 kB

Release files / flask_nobot-1.0.1.tar.gz

Download URL flask_nobot-1.0.1.tar.gz
Size 21.3 kB
Tags Source
SHA-256 checksum
How to use checksums
6b07977e4c04e84bae54293f09565f69dcae03d36f6b8b498d924dbccc1c415e
BLAKE2b-256 checksum
How to use checksums
0488c2d2934a96b3ded0aba5d31b5197d1153608139c16f7cff8a3ebc1b049c9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on May 12, 2026.

Transparency log

Release files / flask_nobot-1.0.1-py3-none-any.whl

Download URL flask_nobot-1.0.1-py3-none-any.whl
Size 19.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b6e7a1380fcb708818b6c7c13cebbdf18b8a4152bc3e236d532fa2e82bd21946
BLAKE2b-256 checksum
How to use checksums
fb6f1c0ea13b101864b7d68370a6f9806421cc3d02886508a6928bd6e65f708e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on May 12, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

1.0.1 This release

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page