Skip to main content

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.

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_SECRETSECRET_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_*, 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

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

flask_nobot-1.0.0.tar.gz (20.6 kB view details)

Uploaded Source

Built Distribution

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

flask_nobot-1.0.0-py3-none-any.whl (18.5 kB view details)

Uploaded Python 3

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

Hashes for flask_nobot-1.0.0.tar.gz
Algorithm Hash digest
SHA256 3479c8ee15e139acc7f03a227376c25209d77cd8c1e3d81048ceb64157ee2f6e
MD5 68bdc96d2a170b756e2ab40679d4ac13
BLAKE2b-256 16dbb8e587abe5cd9356548ea145886ce0d5f2acb533e2a2226c33890ab9068c

See more details on using hashes here.

Provenance

The following attestation bundles were made for flask_nobot-1.0.0.tar.gz:

Publisher: publish.yml on tn3w/flask-nobot

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

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

Hashes for flask_nobot-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 572472a0f98056918ab4413cadbf287446376b0b68553dd5944bee5ead2a2708
MD5 a0a77c94ca78ad296ce41687833e65e2
BLAKE2b-256 e0c0d864441c809935b5ffc3e68dc96c1d00402a5ac219acf3190aff1d1e8c71

See more details on using hashes here.

Provenance

The following attestation bundles were made for flask_nobot-1.0.0-py3-none-any.whl:

Publisher: publish.yml on tn3w/flask-nobot

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