Security

All values passed to QFQ will be:

  • Checked against max. length and allowed content, on the client and on the server side. On the server side, the check happens before any further processing. The ‘length’ and ‘allowed’ content is specified per FormElement. ‘digit’ or ‘alnumx’ is the default. Violating the rules will stop the ‘save record’ process (Form) or result in an empty value (Report). If a variable is not replaced, check the default sanitize class.

  • Only elements defined in the Form definition or requested by Report will be processed.

  • UTF8 normalized (normalizer::normalize) to unify different ways of composing characters. It’s more a database interest, to work with unified data.

SQL statements are typically fired as prepared statements with separated variables. Further custom SQL statements will be defined by the webmaster - those do not use prepared statements and might be affected by SQL injection. To prevent SQL injection, every variable is by default escaped with mysqli::real_escape_string.

QFQ notice:

  • Variables passed by the client (=Browser) are untrusted and use the default sanitize class ‘digit’ (if nothing else is specified). If alpha characters are submitted, the content violates digit and becomes therefore !!<name of sanitize class>!! - there is no error message. Best is to always use SIP (value is trustful) or at least digits for GET (=client) parameter (user might change those and therefore those are not trustful).

Get Parameter

QFQ security restriction:

  • GET parameter might contain urlencoded content (%xx). Therefore all GET parameter will be processed by ‘urldecode()’. As a result a text like ‘%nn’ in GET variables will always be decoded. It’s not possible to transfer ‘%nn’ itself.

  • GET values are limited to securityGetMaxLength (Extension Manager: QFQ Configuration) chars - any violation will stop QFQ. Individual exceptions are defined via Exception for SECURITY_GET_MAX_LENGTH.

  • GET parameter ‘type’ and ‘L’ might affected by (T3, configuration dependent) cache poisoning. If they contain non digit values, only the first character is used (if this is a digit) or completely cleaned (else).

Post Parameter

Per FormElement (HTML input) the default is to htmlspecialchars() the input. This means &<>'" will be encoded as htmlentity and saved as a htmlentity in the database. In case any of these characters (e.g. for HTML tags) are required, the encoding can be disabled per FormElement: encode=none (default is specialchar).

During Form load, htmlentities are decoded again.

$_SERVER

All $_SERVER vars are htmlentities encoded (all, not only specialchars!) .

Honeypot

Every QFQ Form contains ‘honeypot’-HTML input elements (HTML: hidden & readonly). Which of them to use is configured in Configuration (default: ‘username’, ‘password’ and ‘email’). On every start of QFQ (form, report, save, …), these variables are tested if they are non-empty. In such a case a probably malicious bot has send the request and the request will not be processed.

If any of the default configured variable names are needed (which will never be the case for QFQ), an explicit variable name list have to be configured in Configuration.

QFQ security restriction:

  • The honeypot variables can’t be used in GET or POST as regular HTML input elements - any values of them will terminate QFQ.

Violation

On any violation, QFQ will sleep securityAttackDelaySeconds (Configuration) and than exit the running PHP process. A detected attack leads to a complete white (=empty) page.

If securityShowMessage: on (Configuration), at least a message is displayed after the delay.

Client Parameter via SIP

Links with URL parameters, targeting to the local website, are typically SIP encoded. Instead of transferring the parameter as part of the URL, only one unique GET parameter ‘s’ is appended at the link. The parameter ‘s’ is unique (equal to a timestamp) for the user. Assigned variables are stored as a part of the PHP user session on the server. Two users might have the same value of parameter ‘s’, but the content is completely independent.

Variables needed by Typo3 remains on the link and are not ‘sip-encoded’.

Secure direct file access

A check for protected folder access is active as default and in the qfq configuration defined under security.protectedFolderCheck. See: Extension Manager: QFQ Configuration An activated check creates automatically a .htaccess file with following content which denies every connection from outside the server:

Deny from all

Command wget is used for access check. Command can be changed under tools.cmdWget, configured in qfq configuration. See: Extension Manager: QFQ Configuration

If you don’t need the protected folder check, then revise following instructions.

If the application uploads files, mostly it’s not necessary and often a security issue, to offer a direct download of the uploaded files. Best is to create a directory, e.g. <site path>/fileadmin/protected and deny direct access via webbrowser to it. E.g. for Apache set a rule:

<Directory "/var/www/html/fileadmin/protected">
    Require all denied
</Directory>

If you only have access to .htaccess, create a file <site path>/fileadmin/protected/.htaccess with:

<IfModule mod_authz_core.c>
     Require all denied
</IfModule>

Important

All QFQ uploads should save files only in/below such a protected directory.

To offer download of those files, use the reserved column name ‘_download’ (see Download) or variants.

Important

To protect the installation against executing of uploaded malicious script code, disable PHP for the final upload directory. E.g. fileadmin (Apache):

<Directory "/var/www/html/fileadmin">
  php_admin_flag engine Off
</Directory>

This is in general a good security improvement for directories with user supplied content.

File upload

By default the mime type of every uploaded file is checked against a white list of allowed mime types. The mime type of a file can be (easily) faked by an attacker. This check is good to handle regular user file upload for specific file types but won’t help to prevent attacks against uploading and executing malicious code.

Instead prohibit the execution of user contributed files by the webserver config (SecureDirectFileAccess).

Typo3 Setup - best practice

  • Activate notification emails for every BE login (if there are only few BE users). In case the backend has been hacked, unusual login’s (time or username) will appear:

    [BE][warning_email_addr] = <your email>
    [BE][warning_mode] = 1
    

Automated IP banning

This guide explains how to set up Fail2Ban as either a Docker service or a standalone package, including its essential configuration files.

Basic fail2ban setup

Start by creating the necessary folders and files:

-fail2ban/
    -fail2ban.conf
    -action.d/
        -iptables-multiport.conf
    -db/
    -filter.d/
            -qfq.conf
    -jail.d/
            -qfq.conf

fail2ban.conf

The fail2ban.conf file contains the core configuration Fail2Ban needs to operate. This includes logging options, database management, and socket file locations.

Example:

[DEFAULT]
# The level of detail the log provides [ERROR, WARN, INFO, DEBUG]
loglevel = INFO

# Where logs should be writen or shown
logtarget = /var/log/fail2ban.log

# location of the socket file
socket = /var/run/fail2ban/fail2ban.sock

# location of the PID file
pidfile = /var/run/fail2ban/fail2ban.pid

# location of the fail2ban DB
dbfile = /data/db/fail2ban.sqlite3

# How long (in seconds) to keep old (expired) entries in the database
dbpurgeage = 86400

allowipv6 = auto

action.d/iptables-multiport.conf

Files in the action.d/ directory define how Fail2Ban reacts when banning or unbanning IPs. The iptables-multiport.conf file provides an example of using iptables to block offending IPs.

Example:

[Definition]
# What happens when Fail2Ban starts
actionstart = mkdir -p /var/log/qfq && \
              iptables -N f2b-<name> && \
              iptables -A f2b-<name> -j RETURN && \
              iptables -I <chain> -p <protocol> -m multiport --dports <port> -j f2b-<name>

# What happens when Fail2Ban shuts down
actionstop = iptables -D <chain> -p <protocol> -m multiport --dports <port> -j f2b-<name> && \
             iptables -F f2b-<name> && \
             iptables -X f2b-<name>

# What happens when an IP is banned
actionban = iptables -I f2b-<name> 1 -s <ip> -j <blocktype> && \
            if ! grep -q "<ip>" /var/log/qfq/banned_ips.log; then \
                echo "`date '+%%Y-%%m-%%d %%H:%%M:%%S'` - Banned IP: <ip>" >> /var/log/qfq/banned_ips.log; \
            fi

# What happens when an IP is unbanned
actionunban = iptables -D f2b-<name> -s <ip> -j <blocktype> && \
              echo "`date '+%%Y-%%m-%%d %%H:%%M:%%S'` - Unbanned IP: <ip>" >> /var/log/qfq/banned_ips.log

# A pre-check to ensure functionality of the action
actioncheck = iptables -n -L <chain> | grep -q 'f2b-<name>'

[Init]
# Name of the custom chain for this action
name = default

# Ports to monitor and protect
port = 80,443

# Protocol to monitor (TCP or UDP)
protocol = tcp

# The firewall chain to use (e.g., INPUT)
chain = INPUT

# The blocking method (e.g., DROP or REJECT)
blocktype = REJECT --reject-with icmp-port-unreachable

filter.d/qfq.conf

The filter.d/ directory is where you define custom regex patterns to detect malicious activity from log files. These filters determine which IPs should be banned based on matching log entries.

Example:

[Definition]
# Custom regex to identify malicious activity and extract offending IPs
# <HOST> is a placeholder for the IP address, handled by Fail2Ban for both IPv4 and IPv6
#Example: [2025-01-20 11:23:09 / 192.168.133.100 / 998qqk7c86eslkgeb1drkl82il] Security: attack detected
failregex = .* \/ <HOST> \/ .*?Security: attack detected.*

# Optional: Regex to ignore certain matches (e.g., trusted sources)
ignoreregex =

jail.d/qfq.conf

Files in jail.d/ connect filters to actions and define the parameters for banning IPs.

Example:

[qfq]
enabled = true
# Enable or disable this jail. Set to "false" to deactivate.

port = 80,443
# Ports to monitor for malicious activity. Add additional ports as needed.

filter = qfq
# The filter file (defined in `filter.d/`) to use for this jail.

logpath = /var/log/qfq/qfq.log
# The log file to monitor for detecting malicious activity.

maxretry = 3
# The maximum number of failed attempts before banning an IP.

bantime = 3600
# Duration (in seconds) for which an offending IP is banned.

action = iptables-multiport
# Action to execute when banning an IP. Can be customized to include other actions.

Adding Mail notification

New file custom-mail.conf in fail2ban/action.d/.

custom-mail.conf:

[Definition]
actionstart = echo 'Starting jail <name>'
actionstop = echo 'Stopping jail <name>'
actionban = echo "Subject: [Fail2Ban] Ban for <ip>" | sendmail -v <RECIVER MAIL>
actionunban = echo "Subject: [Fail2Ban] Unban for <ip>" | sendmail -v <RECIVER MAIL>

Add custom-mail to jail.d/qfq.conf action. If you are using docker add this to docker-compose.yml:

fail2ban:
...OTHER CONF...
environment:
  - TZ=<TIMEZONE>
  - F2B_DEST_EMAIL=<EMAIL RECEVER>
  - F2B_SENDER_EMAIL=<EMAIL SENDER
  - F2B_MTA=sendmail
cap_add:
...OTHER CONF...

Docker Setup

When setting up Fail2Ban in a Docker environment, ensure that log files and important directories (e.g., /var/log/qfq/) are correctly mounted using docker-compose.yml.

Example configuration:

fail2ban:
  image: crazymax/fail2ban:latest
  container_name: fail2ban_<NAME>
  volumes:
    - ./fail2ban:/etc/fail2ban
    - ./html/fileadmin/protected/qfqProject/log:/var/log/qfq
    - ./fail2ban/db:/data/db
    - /lib/modules:/lib/modules:ro
    - /usr/sbin/iptables-legacy:/usr/sbin/iptables:ro
    - /usr/sbin/ip6tables-legacy:/usr/sbin/ip6tables:ro
  cap_add:
    - NET_ADMIN
  network_mode: host
  restart: unless-stopped

Now Start Docker:

# Docker is not running
docker compose up -d

# Docker is allready running
docker compose down && docker compose up -d

Check if Container is running:

# Show running docker containers
Docker ps

# Show docker logs
Docker logs <CONTAINER NAME>

Package Setup

Install Fail2Ban via your system’s package manager:

sudo apt update
sudo apt install fail2ban

Place the configuration files in their respective directories:

fail2ban.conf -> /etc/fail2ban/
Filters -> /etc/fail2ban/filter.d/
Actions -> /etc/fail2ban/action.d/
Jails -> /etc/fail2ban/jail.d/

Start the service:

systemctl enable fail2ban
systemctl start fail2ban

# Test configuration
sudo fail2ban-client --test

# Check active Jails
sudo fail2ban-client status qfq

Script to get currently banned IPs

find_banned_ips.sh:

#!/bin/bash

# Function to find all Fail2Ban chains
get_fail2ban_chains() {
    iptables -L | grep -oE '^Chain f2b-[^ ]+' | awk '{print $2}'
}

# Function to list banned IPs from a specific chain
get_banned_ips_from_chain() {
    local chain=$1
    iptables -L "$chain" -n | awk '/DROP|REJECT/ {print $4}'
}

# Main script
echo "Finding banned or redirected IPs from iptables..."
echo "=============================================="

# Get all Fail2Ban chains
chains=$(get_fail2ban_chains)

if [[ -z "$chains" ]]; then
    echo "No Fail2Ban chains found in iptables."
    exit 1
fi

# Iterate through each chain and get banned IPs
for chain in $chains; do
    echo "Banned IPs in chain: $chain"
    banned_ips=$(get_banned_ips_from_chain "$chain")
    if [[ -n "$banned_ips" ]]; then
        echo "$banned_ips"
    else
        echo "No banned IPs in this chain."
    fi
    echo "----------------------------------------------"
done