Lightweight, zero-dependency bot protection for Flask applications.
Project description
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, 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
Project details
Release history Release notifications | RSS feed
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 flask_nobot-1.0.0.tar.gz.
File metadata
- Download URL: flask_nobot-1.0.0.tar.gz
- Upload date:
- Size: 20.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3479c8ee15e139acc7f03a227376c25209d77cd8c1e3d81048ceb64157ee2f6e
|
|
| MD5 |
68bdc96d2a170b756e2ab40679d4ac13
|
|
| BLAKE2b-256 |
16dbb8e587abe5cd9356548ea145886ce0d5f2acb533e2a2226c33890ab9068c
|
Provenance
The following attestation bundles were made for flask_nobot-1.0.0.tar.gz:
Publisher:
publish.yml on tn3w/flask-nobot
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
flask_nobot-1.0.0.tar.gz -
Subject digest:
3479c8ee15e139acc7f03a227376c25209d77cd8c1e3d81048ceb64157ee2f6e - Sigstore transparency entry: 1328072663
- Sigstore integration time:
-
Permalink:
tn3w/flask-nobot@545461eb18c5ebc9b4aa73b02f25685347d75531 -
Branch / Tag:
refs/heads/master - Owner: https://github.com/tn3w
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@545461eb18c5ebc9b4aa73b02f25685347d75531 -
Trigger Event:
push
-
Statement type:
File details
Details for the file flask_nobot-1.0.0-py3-none-any.whl.
File metadata
- Download URL: flask_nobot-1.0.0-py3-none-any.whl
- Upload date:
- Size: 18.5 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 |
572472a0f98056918ab4413cadbf287446376b0b68553dd5944bee5ead2a2708
|
|
| MD5 |
a0a77c94ca78ad296ce41687833e65e2
|
|
| BLAKE2b-256 |
e0c0d864441c809935b5ffc3e68dc96c1d00402a5ac219acf3190aff1d1e8c71
|
Provenance
The following attestation bundles were made for flask_nobot-1.0.0-py3-none-any.whl:
Publisher:
publish.yml on tn3w/flask-nobot
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
flask_nobot-1.0.0-py3-none-any.whl -
Subject digest:
572472a0f98056918ab4413cadbf287446376b0b68553dd5944bee5ead2a2708 - Sigstore transparency entry: 1328072684
- Sigstore integration time:
-
Permalink:
tn3w/flask-nobot@545461eb18c5ebc9b4aa73b02f25685347d75531 -
Branch / Tag:
refs/heads/master - Owner: https://github.com/tn3w
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@545461eb18c5ebc9b4aa73b02f25685347d75531 -
Trigger Event:
push
-
Statement type: