flask-NoBot
Lightweight, zero-dependency bot protection for Flask. Invisible JS challenge (no CAPTCHA), regex/CIDR rule engine, HMAC-signed verification tokens.
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_*,webdriverattr - Native
Function.toStringintegrity - 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=Strictcookies- Required
Origin+X-Requested-Withcheck 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
ris consumed server-side on first verify → replay blocked fornonce_ttl - Crawler heuristic (
is_crawler): regex over UA for bot signals, known tools, URLs-in-UA, absence of real browser tokens → addscrawler_weightto score - Bogon detection (
is_bogon): blocks RFC1918/loopback/link-local/reserved ranges whendeny_bogons=True(enable behind trusted proxy)
Formatting
pip install black isort
isort . && black .
npx prtfm
License
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)
| File | Size | Uploaded | |
|---|---|---|---|
| flask_nobot-1.0.1.tar.gz | 21.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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 logRelease 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