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, 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

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.1.tar.gz (21.3 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.1-py3-none-any.whl (19.1 kB view details)

Uploaded Python 3

File details

Details for the file flask_nobot-1.0.1.tar.gz.

File metadata

  • Download URL: flask_nobot-1.0.1.tar.gz
  • Upload date:
  • Size: 21.3 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.1.tar.gz
Algorithm Hash digest
SHA256 6b07977e4c04e84bae54293f09565f69dcae03d36f6b8b498d924dbccc1c415e
MD5 12ff7de6d580f25b100284e9475617cc
BLAKE2b-256 0488c2d2934a96b3ded0aba5d31b5197d1153608139c16f7cff8a3ebc1b049c9

See more details on using hashes here.

Provenance

The following attestation bundles were made for flask_nobot-1.0.1.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.1-py3-none-any.whl.

File metadata

  • Download URL: flask_nobot-1.0.1-py3-none-any.whl
  • Upload date:
  • Size: 19.1 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.1-py3-none-any.whl
Algorithm Hash digest
SHA256 b6e7a1380fcb708818b6c7c13cebbdf18b8a4152bc3e236d532fa2e82bd21946
MD5 34392a18cc3a3200d470b74076921190
BLAKE2b-256 fb6f1c0ea13b101864b7d68370a6f9806421cc3d02886508a6928bd6e65f708e

See more details on using hashes here.

Provenance

The following attestation bundles were made for flask_nobot-1.0.1-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