Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,108 @@
then silently never blocked anything. IP subnet matching for block and
allow list entries is now done in PHP instead of with MySQL-only SQL
functions, and fixed-length time windows are computed in PHP
- Security: an emailed unblock token no longer lets a blocked IP address
bypass the block. It was accepted on any request with
view=tokenunblock (e.g. a login), was never used up by such a request
and was not checked for expiry, so anyone who got a token (e.g. by
getting their own account blocked) could keep guessing passwords from
a blocked IP. Now it only passes the unblock page of com_bfstop itself,
only while unexpired, and only if issued for a block of the requesting
IP address; the token is no longer written to the log either
- Security: behind a proxy/load balancer, the client IP is now taken from
the end of the forwarding header (the entry added by the trusted proxy)
instead of its first public entry, which a visitor could forge to evade
blocking, get other addresses blocked or impersonate an allowlisted one.
Several trusted proxies (and CIDR subnets) can be configured, the
"Forwarded" header (RFC 7239) and addresses with ports are understood,
and private client addresses reported by the proxy are accepted. Not
all proxies append to an incoming header: pick a header yours sets or
appends to
- Security: a failed login is now recorded, counted and checked against
the block threshold before the configured delay is applied, not after
it, and a request whose IP address just got (or already is) blocked is
not delayed any more. Before, a burst of parallel login attempts all
ran before the first of them was counted, and every one of them held
on to a server worker for the whole delay
- Security: usernames of failed logins which do not belong to an existing
account (and are not on the list of common usernames) are now stored,
shown, logged and mailed as an unreadable hash by default, as users
often type their password into the username field; the hash is the same
for the same input, so attempts are still counted together. The new
setting "Usernames Not Matching an Account" restores the old behaviour.
Entries stored by earlier versions stay as they are - purge them in the
failed login and username statistics views if you want them gone
- Security: unblocking by the link in the notification email now needs a
confirmation (a POST request) and only works from the IP address which
was blocked, so mail security scanners or link previews which open
every link in a message can't unblock an attacker (see com_bfstop)
- Security: the "User Block Message" with the unblock link is no longer
sent if the failed logins from the blocked IP address which led to the
block also went against other existing accounts. The link goes to the
owner of the account of the last attempt, so an attacker with an account
of their own could otherwise end every series of guesses at other
accounts with an attempt at their own, get the link and continue.
Attempts against names which don't exist (e.g. typos) don't count
- Security: anybody can make the plugin send the "User Block Message" to
a user, by failing to log in with that username from an address until it
is blocked - a way to flood somebody's inbox. The setting now has three
values: disabled; enabled only if the user has logged in from the
blocked IP address (or, for IPv6, its network) before, which is the
case for somebody locking themselves out but never for an attacker's
address; and enabled for any IP address, where a user gets at most one
unblock link at a time which can still be used (not yet expired or used
up). Note that the previous "enabled" now means the first of these; the
old behaviour is the third. Known IP addresses are recorded at
successful logins, so users get the message again after having logged
in once. The username a link was sent to is recorded with its token
- The addresses users logged in from (used for the risk score and for the
unblock message) are remembered by network for IPv6 clients, as failed
logins are, so a client whose address changes within its /64 every day
stays a known one and doesn't add a row per address; the lookup ignores
the case of the username. They are forgotten a year after the last login
from them (and the table is limited to 100000 rows, oldest first). This
maintenance, like the limit of the username statistics, runs once a day
whatever the purge age is set to
- Unblock tokens which have expired are deleted by the daily maintenance too
(before: only when somebody visited the unblock page, or by the purge by
age if one is configured). The status code of the block page is set with
http_response_code() instead of a raw "HTTP/1.0 403" header, which hard-coded
the protocol version
- The unblock page of com_bfstop answers with an error status if the link
can't be used (400 without a token, 403 from another IP address, 404 for
an unknown, expired or used token, 500 if unblocking failed), unless "Use
HTTP Error" is switched off; opening the link (a GET request) stays a 200
for every token. The page is not cached and tells browsers not to send
its address (which contains the token) to other sites and search engines
not to index it
- Security: IPv6 clients are now tracked per network (default: /64, see
the new "IPv6 Tracking Granularity" setting) instead of per address:
failed logins are counted and blocks apply to the whole network, so an
attacker can no longer start over by switching to another address in
their /64. IPv4-mapped, NAT64, 6to4 and Teredo addresses, which embed
unrelated clients' IPv4 addresses, are still tracked individually
- Security: the block page is never cached (Cache-Control: no-store) and,
if no setting says otherwise, answers with the HTTP status 403 instead of
200, so a caching proxy/CDN can't serve it to other visitors
- Security: only a valid IP address or subnet is written to the .htaccess
file (before, a value like "192.0.2.0/1e1" passed the backend validation
and made Apache reject the whole file); the 403 message is escaped; and
changes of the file are done while holding a lock, so that clients
getting blocked at the same time don't overwrite each other's lines
- Security: the plugin's log file is rotated at 5 MB, and the username
statistics (which aren't purged by age) are limited to 10000 usernames,
removing those with the fewest attempts first, so a visitor can't fill up
disk or database by making up usernames
- Security: the periodic maintenance (once a day) only updates its own
timestamp in the plugin's stored settings, instead of writing back all
settings as they were when the request started, which could undo a
change an administrator saved in the meantime; and it now also runs if
no purge age is configured
- Security (component): sending the test email now needs the permission to
change the settings; the .htaccess path and GeoIP database path settings
are validated (no stream wrappers like phar://, a .mmdb file name)
- Added SECURITY.md with the supported versions and how to report
vulnerabilities

1.5.2 (2024-02-18)
- Note: Only component changes, no plugin changes
Expand Down
38 changes: 37 additions & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
@@ -1 +1,37 @@
With information on security vulnerabilities, please contact me via security@bfstop.de.
# Security Policy

## Supported versions

Security fixes are made for the latest release of the BFStop plugin
(`plg_system_bfstop`) and the BFStop component (`com_bfstop`). The two are
released together and need to be on the same major and minor version, so
please update both.

## Reporting a vulnerability

Please report suspected vulnerabilities by email to **security@bfstop.de**
rather than in a public issue or pull request, so that a fix can be
available before details are public.

It helps to include:

- the affected version(s) of plugin and component, and the Joomla! and PHP
versions,
- the relevant settings (blocking mode, proxy settings, ...),
- steps to reproduce, or a proof of concept, and what an attacker gains.

## What is and isn't in scope

BFStop sits in front of the login of a Joomla! site and decides which
requests to reject, so these are the kind of reports which are most useful:

- ways around a block or the failed-login counting (for example by faking
the client address, or by reusing an unblock link),
- ways to get someone else blocked, or to make the plugin tie up the server,
- ways to read or change data (settings, blocks, logged usernames) without
the necessary permissions, and cross-site scripting in the backend views.

Settings are changed by users with the `core.admin` permission for
`com_bfstop`. They can, among other things, choose the directory in which the
plugin writes its `.htaccess` file, so grant that permission only to people
you trust with the web server's configuration for your site.
6 changes: 4 additions & 2 deletions language/de-DE/de-DE.plg_system_bfstop.sys.ini
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,11 @@ PLG_SYSTEM_BFSTOP_BLOCK_DURATION_DESC="Bestimmt wie lange die IP-Sperre aufrecht
PLG_SYSTEM_BFSTOP_NOTIFY_REMAINING_ATTEMPTS_LABEL="Übrige Versuche"
PLG_SYSTEM_BFSTOP_NOTIFY_REMAINING_ATTEMPTS_DESC="Schaltet die Benachrichtigung über verbleibende Versuche ein; wenn aktiviert, wird im Login-Formular nach einem fehlgeschlagenen Login-Versuch eine Nachricht mit den verbleibenden Login-Versuchen angezeigt. Diese Einstellung wird nur dann berücksichtigt, wenn Sperren eingeschaltet ist (wenn es nicht eingeschaltet ist, gibt es ja auch keine 'verbleibenden' Versuche)"
PLG_SYSTEM_BFSTOP_NOTIFY_BLOCKED_USER_LABEL="Benutzer-Block-Nachricht"
PLG_SYSTEM_BFSTOP_NOTIFY_BLOCKED_USER_DESC="Wenn eingeschaltet, wird an die in der Datenbank hinterlegte Email-Adresse eine Nachricht hinterlegt, wenn eine IP-Adresse gesperrt wird (sofern der Benutzername, mit dem das Login versucht wurde, ein existierender war)"
PLG_SYSTEM_BFSTOP_NOTIFY_BLOCKED_USER_DESC="Wenn eingeschaltet, erhält ein Benutzer, dessen Benutzername bei fehlgeschlagenen Logins von einer später gesperrten IP-Adresse verwendet wurde, eine Email mit einem Link zum Entsperren dieser Adresse. Es wird nichts gesendet, wenn der Benutzername nicht existiert oder wenn die fehlgeschlagenen Logins von dieser Adresse auch gegen andere existierende Konten gerichtet waren (der Link darf nicht an den Inhaber eines Kontos gehen, wenn von dort aus andere angegriffen werden). Jeder kann eine solche Email auslösen, indem er sich erfolglos als dieser Benutzer anmeldet: 'Nur wenn sich der Benutzer schon von dieser IP-Adresse aus angemeldet hat' (empfohlen) verhindert, dass dies gegen deine Benutzer eingesetzt wird; wird die Email für jede IP-Adresse gesendet, bekommt ein Benutzer höchstens einen noch verwendbaren Link auf einmal."
PLG_SYSTEM_BFSTOP_NOTIFY_BLOCKED_USER_KNOWN="eingeschaltet, nur wenn sich der Benutzer schon von dieser IP-Adresse aus angemeldet hat (empfohlen)"
PLG_SYSTEM_BFSTOP_NOTIFY_BLOCKED_USER_ANY="eingeschaltet, für jede IP-Adresse (jeder kann damit eine Email an einen Benutzer auslösen)"
PLG_SYSTEM_BFSTOP_USE_HTTP_ERROR_LABEL="Verwende HTTP-Fehler"
PLG_SYSTEM_BFSTOP_USE_HTTP_ERROR_DESC="Wenn aktiviert, wird der HTTP Statuscode 403 (Forbidden) gesendet wenn die IP-Addresse eines Benutzers gesperrt ist."
PLG_SYSTEM_BFSTOP_USE_HTTP_ERROR_DESC="Wenn aktiviert (empfohlen), beantwortet bfstop Anfragen einer gesperrten IP-Adresse mit dem HTTP-Statuscode 403 (Forbidden) und die Entsperrseite des Links in der Benachrichtigungs-Email mit einem Fehlerstatus, wenn der Link nicht verwendet werden kann (400, 403, 404 oder 500). Deaktiviere dies nur, wenn dein Webserver, CDN oder deine Firewall Fehlerseiten durch eigene ersetzt und Benutzer die Meldung deshalb nie zu sehen bekommen: Sie wird dann mit dem Statuscode 200 gesendet. In beiden Fällen werden die Seiten nie zwischengespeichert."
PLG_SYSTEM_BFSTOP_ENABLED="aktiviert"
PLG_SYSTEM_BFSTOP_DISABLED="deaktiviert"
PLG_SYSTEM_BFSTOP_NOTIFY_UNLIMITED="unbegrenzt"
Expand Down
10 changes: 6 additions & 4 deletions language/en-GB/en-GB.plg_system_bfstop.sys.ini
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,11 @@ PLG_SYSTEM_BFSTOP_BLOCK_DURATION_DESC="Determines how long an IP address is bloc
PLG_SYSTEM_BFSTOP_NOTIFY_REMAINING_ATTEMPTS_LABEL="Remaining attempts"
PLG_SYSTEM_BFSTOP_NOTIFY_REMAINING_ATTEMPTS_DESC="Enables or disables notifications about remaining attempts; if enabled, the user will, after each failed login attempt, see a message in the login form about how many login attempts still are available before the IP address will be blocked. This setting is only active if blocking is enabled (if blocking is disabled, 'remaining' entries wouldn't make sense)."
PLG_SYSTEM_BFSTOP_NOTIFY_BLOCKED_USER_LABEL="User Block Message"
PLG_SYSTEM_BFSTOP_NOTIFY_BLOCKED_USER_DESC="When turned on, in case of a blocked IP address, a notification will be sent to the user which tried to log in (only of course in case it is an existing username)."
PLG_SYSTEM_BFSTOP_NOTIFY_BLOCKED_USER_DESC="When turned on, a user whose username was used in failed logins from an IP address which then got blocked receives an email with a link to unblock that address. Nothing is sent if the username does not exist, or if the failed logins from that address also went against other existing accounts (the link must not go to the owner of one account if somebody is attacking others from there). Anybody can cause such an email by failing to log in as that user: 'only if the user has logged in from that IP address before' (recommended) keeps this from being used against your users; if emails are sent for any IP address, at most one link which can still be used is sent to a user at a time."
PLG_SYSTEM_BFSTOP_NOTIFY_BLOCKED_USER_KNOWN="enabled, only if the user has logged in from that IP address before (recommended)"
PLG_SYSTEM_BFSTOP_NOTIFY_BLOCKED_USER_ANY="enabled, for any IP address (anybody can make this send an email to a user)"
PLG_SYSTEM_BFSTOP_USE_HTTP_ERROR_LABEL="Use HTTP Error"
PLG_SYSTEM_BFSTOP_USE_HTTP_ERROR_DESC="If enabled, bfstop will send a status code 403 (Forbidden) if the users' IP address is blocked"
PLG_SYSTEM_BFSTOP_USE_HTTP_ERROR_DESC="If enabled (recommended), bfstop answers requests from a blocked IP address with the status code 403 (Forbidden), and the unblock page of the link in the notification email with an error status when the link can't be used (400, 403, 404 or 500). Switch this off only if your web server, CDN or firewall replaces error pages with its own, so that users never get to see the message: it is then sent with status 200 instead. Either way, the pages are never cached."
PLG_SYSTEM_BFSTOP_ENABLED="enabled"
PLG_SYSTEM_BFSTOP_DISABLED="disabled"
PLG_SYSTEM_BFSTOP_NOTIFY_UNLIMITED="unlimited"
Expand Down Expand Up @@ -76,9 +78,9 @@ PLG_SYSTEM_BFSTOP_PROXY_FIELDSET_LABEL="Proxy / Load Balancer"
PLG_SYSTEM_BFSTOP_USE_PROXY_LABEL="Behind a proxy / load balancer"
PLG_SYSTEM_BFSTOP_USE_PROXY_DESC="Enable this only if your site sits behind a reverse proxy or load balancer, so that the visitor's browser never connects to Joomla! directly. When enabled, bfstop reads the visitor's IP address from the header selected below instead of from the direct TCP connection - but only for requests that actually come from the proxy IP address configured below. Leave disabled unless you have such a setup: any visitor can forge this header, so trusting it without a verified proxy IP would let an attacker get an arbitrary IP address blocked instead of their own."
PLG_SYSTEM_BFSTOP_PROXY_HEADER_SOURCE_LABEL="Client IP header"
PLG_SYSTEM_BFSTOP_PROXY_HEADER_SOURCE_DESC="The HTTP header your proxy/load balancer uses to report the original client IP address. Check your proxy's documentation if unsure - X-Forwarded-For is the most common choice."
PLG_SYSTEM_BFSTOP_PROXY_HEADER_SOURCE_DESC="The HTTP header your proxy/load balancer uses to report the original client IP address. Check your proxy's documentation if unsure - X-Forwarded-For is the most common choice. The client address is taken from the end of the header, i.e. the entry added by your own proxy; entries further left are supplied by the client and ignored. Choose a header your proxy sets or appends to itself."
PLG_SYSTEM_BFSTOP_PROXY_IP_LABEL="Proxy / load balancer IP address"
PLG_SYSTEM_BFSTOP_PROXY_IP_DESC="The IP address of your reverse proxy or load balancer, i.e. the address Joomla! sees as REMOTE_ADDR for every legitimate request. The client IP header above is only trusted for requests coming from this address, so that a visitor cannot bypass the proxy and spoof the header directly. Required if 'Behind a proxy / load balancer' is enabled above."
PLG_SYSTEM_BFSTOP_PROXY_IP_DESC="The IP address of your reverse proxy or load balancer, i.e. the address Joomla! sees as REMOTE_ADDR for every legitimate request. If there are several proxies in a row (e.g. a CDN in front of a load balancer), list all of them, separated by commas; subnets in CIDR notation (e.g. 10.0.0.0/8) are allowed too. The client IP header above is only trusted for requests coming from one of these addresses, so that a visitor cannot bypass the proxy and spoof the header directly. Required if 'Behind a proxy / load balancer' is enabled above."
PLG_SYSTEM_BFSTOP_GROUP_NOTIFICATION_ENABLED_LABEL="User group notification"
PLG_SYSTEM_BFSTOP_GROUP_NOTIFICATION_ENABLED_DESC="If enabled, the whole user group selected below will be notified in case of failed logins/banned IPs"
PLG_SYSTEM_BFSTOP_USERGROUP_LABEL="User group"
Expand Down
2 changes: 2 additions & 0 deletions sql/install.mysql.utf8.sql
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,12 @@ CREATE TABLE IF NOT EXISTS #__bfstop_unblock (

-- stores randomized tokens for unblocking an IP via an email
-- to the blocked user
-- username: the user the unblock link was sent to, to limit the links per user
CREATE TABLE IF NOT EXISTS #__bfstop_unblock_token (
token varchar(40) NOT NULL,
block_id int(10) NOT NULL,
crdate datetime NOT NULL,
username varchar(150) NULL DEFAULT NULL,
PRIMARY KEY (token)
) DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE=utf8mb4_unicode_ci;

Expand Down
2 changes: 2 additions & 0 deletions sql/install.postgresql.utf8.sql
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,12 @@ CREATE TABLE IF NOT EXISTS "#__bfstop_unblock" (
);


-- username: the user the unblock link was sent to, to limit the links per user
CREATE TABLE IF NOT EXISTS "#__bfstop_unblock_token" (
"token" varchar(40) NOT NULL,
"block_id" integer NOT NULL,
"crdate" timestamp without time zone NOT NULL,
"username" varchar(150) NULL DEFAULT NULL,
PRIMARY KEY ("token")
);

Expand Down
3 changes: 3 additions & 0 deletions sql/updates/2.0.0.sql
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ ALTER TABLE `#__bfstop_unblock` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4

ALTER TABLE `#__bfstop_unblock_token` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- the user an unblock link was sent to, to limit the links per user
ALTER TABLE `#__bfstop_unblock_token` ADD COLUMN username varchar(150) NULL DEFAULT NULL;

ALTER TABLE `#__bfstop_allowlist` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- tables introduced in 2.0.0, see install.mysql.utf8.sql for details
Expand Down
Loading
Loading