Securing Apache with mod_security

Protecting your site with OWASP (Open Web Application Security Project)

The internet today is an insecure place. It’s important to have a good 1st line of defence against web application attacks.

Using mod_security can be tricky with many false positives. Make sure to keep an eye on your log files when activating this module!

Installing the module

# apt install libapache2-mod-security2

If you are planning to use it for a wordpress site, there are a few rules to activate. i use the following ruleset inside the Apache Directive Block of ISPConfig vhost.

<IfModule mod_security2.c>
	SecRuleEngine On
	SecRequestBodyAccess On
	SecResponseBodyAccess On 
	SecResponseBodyMimeType text/plain text/html text/xml application/octet-stream 
	SecResponseBodyLimit 546870912

	<locationmatch "/wp-json/wp/v2/posts/">
		SecRuleRemoveById 930100
		SecRuleRemoveById 930110
		SecRuleRemoveById 949110
		SecRuleRemoveById 980130
	</locationmatch>

SecRule &TX:crs_exclusions_wordpress|TX:crs_exclusions_wordpress "@eq 0" \
    "id:9002000,\
    phase:1,\
    pass,\
    t:none,\
    nolog,\
    ver:'OWASP_CRS/3.2.0',\
    skipAfter:END-WORDPRESS"

SecRule &TX:crs_exclusions_wordpress|TX:crs_exclusions_wordpress "@eq 0" \
    "id:9002001,\
    phase:2,\
    pass,\
    t:none,\
    nolog,\
    ver:'OWASP_CRS/3.2.0',\
    skipAfter:END-WORDPRESS"


#
# -=[ WordPress Front-End ]=-
#


#
# [ Login form ]
#

# User login password
SecRule REQUEST_FILENAME "@endsWith /wp-login.php" \
    "id:9002100,\
    phase:2,\
    pass,\
    t:none,\
    nolog,\
    ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:pwd,\
    ver:'OWASP_CRS/3.2.0'"

# Reset password
SecRule REQUEST_FILENAME "@endsWith /wp-login.php" \
    "id:9002120,\
    phase:2,\
    pass,\
    t:none,\
    nolog,\
    ver:'OWASP_CRS/3.2.0',\
    chain"
    SecRule ARGS:action "@streq resetpass" \
        "t:none,\
        chain"
        SecRule &ARGS:action "@eq 1" \
            "t:none,\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:pass1,\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:pass1-text,\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:pass2"


#
# [ Comments ]
#

# Post comment
SecRule REQUEST_FILENAME "@endsWith /wp-comments-post.php" \
    "id:9002130,\
    phase:2,\
    pass,\
    t:none,\
    nolog,\
    ctl:ruleRemoveTargetById=931130;ARGS:url,\
    ver:'OWASP_CRS/3.2.0'"


#
# [ Gutenberg Editor ]
# Used when a user (auto)saves a post/page with Gutenberg.
#

# Gutenberg
SecRule REQUEST_FILENAME "@rx /wp-json/wp/v[0-9]+/(?:posts|pages)" \
    "id:9002140,\
    phase:1,\
    pass,\
    t:none,\
    nolog,\
    ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:content,\
    ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:json.content,\
    ver:'OWASP_CRS/3.2.0'"

# Gutenberg via rest_route for sites without pretty permalinks
SecRule REQUEST_FILENAME "@endsWith /index.php" \
    "id:9002141,\
    phase:1,\
    pass,\
    t:none,\
    nolog,\
    ver:'OWASP_CRS/3.2.0',\
    chain"
    SecRule &ARGS:rest_route "@eq 1" \
        "t:none,\
        nolog,\
        chain"
        SecRule ARGS:rest_route "@rx ^/wp/v[0-9]+/(?:posts|pages)" \
            "t:none,\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:content,\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:json.content"

#
# [ Live preview ]
# Used when an administrator customizes the site and previews the result
# as a normal user.
#

# Theme select
# Example: wp_customize=on&theme=twentyfifteen&customized=
# {"old_sidebars_widgets_data":{"wp_inactive_widgets":[],
# "sidebar-1":["search-2","recent-posts-2","recent-comments-2",
# "archives-2","categories-2","meta-2"]}}&nonce=XXX&
# customize_messenger_channel=preview-0
SecRule ARGS:wp_customize "@streq on" \
    "id:9002150,\
    phase:2,\
    pass,\
    t:none,\
    nolog,\
    ver:'OWASP_CRS/3.2.0',\
    chain"
    SecRule &ARGS:action "@eq 0" \
        "t:none,\
        ctl:ruleRemoveTargetById=942200;ARGS:customized,\
        ctl:ruleRemoveTargetById=942260;ARGS:customized,\
        ctl:ruleRemoveTargetById=942300;ARGS:customized,\
        ctl:ruleRemoveTargetById=942330;ARGS:customized,\
        ctl:ruleRemoveTargetById=942340;ARGS:customized,\
        ctl:ruleRemoveTargetById=942370;ARGS:customized,\
        ctl:ruleRemoveTargetById=942430;ARGS:customized,\
        ctl:ruleRemoveTargetById=942431;ARGS:customized,\
        ctl:ruleRemoveTargetById=942460;ARGS:customized"

# Appearance -> Widgets -> Live Preview
SecRule ARGS:wp_customize "@streq on" \
    "id:9002160,\
    phase:2,\
    pass,\
    t:none,\
    nolog,\
    ver:'OWASP_CRS/3.2.0',\
    chain"
    SecRule ARGS:action "@rx ^(?:|customize_save|update-widget)$" \
        "t:none,\
        chain"
        SecRule &ARGS:action "@eq 1" \
            "t:none,\
            ctl:ruleRemoveTargetById=942200;ARGS:customized,\
            ctl:ruleRemoveTargetById=942260;ARGS:customized,\
            ctl:ruleRemoveTargetById=942300;ARGS:customized,\
            ctl:ruleRemoveTargetById=942330;ARGS:customized,\
            ctl:ruleRemoveTargetById=942340;ARGS:customized,\
            ctl:ruleRemoveTargetById=942370;ARGS:customized,\
            ctl:ruleRemoveTargetById=942430;ARGS:customized,\
            ctl:ruleRemoveTargetById=942431;ARGS:customized,\
            ctl:ruleRemoveTargetById=942460;ARGS:customized,\
            ctl:ruleRemoveTargetById=920230;ARGS:partials,\
            ctl:ruleRemoveTargetById=941320;ARGS:partials,\
            ctl:ruleRemoveTargetById=942180;ARGS:partials,\
            ctl:ruleRemoveTargetById=942200;ARGS:partials,\
            ctl:ruleRemoveTargetById=942260;ARGS:partials,\
            ctl:ruleRemoveTargetById=942330;ARGS:partials,\
            ctl:ruleRemoveTargetById=942340;ARGS:partials,\
            ctl:ruleRemoveTargetById=942370;ARGS:partials,\
            ctl:ruleRemoveTargetById=942430;ARGS:partials,\
            ctl:ruleRemoveTargetById=942431;ARGS:partials,\
            ctl:ruleRemoveTargetById=942460;ARGS:partials"



# Self calls to wp-cron.php?doing_wp_cron=[timestamp]
# These requests may be missing Accept, Content-Length headers.
# This rule must run in phase:1.
SecRule REQUEST_FILENAME "@endsWith /wp-cron.php" \
    "id:9002200,\
    phase:1,\
    pass,\
    t:none,\
    nolog,\
    ctl:ruleRemoveById=920180,\
    ctl:ruleRemoveById=920300,\
    ver:'OWASP_CRS/3.2.0'"


#
# [ Cookies ]

# WP Session Manager
# Cookie: _wp_session=[hex]||[timestamp]||[timestamp]
# detected SQLi using libinjection with fingerprint 'n&1'
SecRule REQUEST_COOKIES:_wp_session "@rx ^[0-9a-f]+\|\|\d+\|\|\d+$" \
    "id:9002300,\
    phase:1,\
    pass,\
    t:none,\
    nolog,\
    ver:'OWASP_CRS/3.2.0',\
    chain"
    SecRule &REQUEST_COOKIES:_wp_session "@eq 1" \
        "t:none,\
        ctl:ruleRemoveTargetById=942100;REQUEST_COOKIES:_wp_session"


#
# -=[ WordPress Administration Back-End (wp-admin) ]=-
#

# Skip this section for performance unless /wp-admin/ is in filename

SecRule REQUEST_FILENAME "!@contains /wp-admin/" \
    "id:9002400,\
    phase:1,\
    pass,\
    t:none,\
    nolog,\
    ver:'OWASP_CRS/3.2.0',\
    skipAfter:END-WORDPRESS-ADMIN"

SecRule REQUEST_FILENAME "!@contains /wp-admin/" \
    "id:9002401,\
    phase:2,\
    pass,\
    t:none,\
    nolog,\
    ver:'OWASP_CRS/3.2.0',\
    skipAfter:END-WORDPRESS-ADMIN"


#
# [ Installation ]
#

# WordPress installation: exclude database password
SecRule REQUEST_FILENAME "@endsWith /wp-admin/setup-config.php" \
    "id:9002410,\
    phase:2,\
    pass,\
    t:none,\
    nolog,\
    ver:'OWASP_CRS/3.2.0',\
    chain"
    SecRule ARGS:step "@streq 2" \
        "t:none,\
        chain"
        SecRule &ARGS:step "@eq 1" \
            "t:none,\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:pwd"

# WordPress installation: exclude admin password
SecRule REQUEST_FILENAME "@endsWith /wp-admin/install.php" \
    "id:9002420,\
    phase:2,\
    pass,\
    t:none,\
    nolog,\
    ver:'OWASP_CRS/3.2.0',\
    chain"
    SecRule ARGS:step "@streq 2" \
        "t:none,\
        chain"
        SecRule &ARGS:step "@eq 1" \
            "t:none,\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:admin_password,\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:admin_password2,\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:pass1-text"


#
# [ User management ]
#

# Edit logged-in user
SecRule REQUEST_FILENAME "@endsWith /wp-admin/profile.php" \
    "id:9002520,\
    phase:2,\
    pass,\
    t:none,\
    nolog,\
    ver:'OWASP_CRS/3.2.0',\
    chain"
    SecRule ARGS:action "@streq update" \
        "t:none,\
        chain"
        SecRule &ARGS:action "@eq 1" \
            "t:none,\
            ctl:ruleRemoveTargetById=931130;ARGS:url,\
            ctl:ruleRemoveTargetById=931130;ARGS:facebook,\
            ctl:ruleRemoveTargetById=931130;ARGS:instagram,\
            ctl:ruleRemoveTargetById=931130;ARGS:linkedin,\
            ctl:ruleRemoveTargetById=931130;ARGS:myspace,\
            ctl:ruleRemoveTargetById=931130;ARGS:pinterest,\
            ctl:ruleRemoveTargetById=931130;ARGS:soundcloud,\
            ctl:ruleRemoveTargetById=931130;ARGS:tumblr,\
            ctl:ruleRemoveTargetById=931130;ARGS:youtube,\
            ctl:ruleRemoveTargetById=931130;ARGS:wikipedia,\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:pass1,\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:pass1-text,\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:pass2"

# Edit user
SecRule REQUEST_FILENAME "@endsWith /wp-admin/user-edit.php" \
    "id:9002530,\
    phase:2,\
    pass,\
    t:none,\
    nolog,\
    ver:'OWASP_CRS/3.2.0',\
    chain"
    SecRule ARGS:action "@streq update" \
        "t:none,\
        chain"
        SecRule &ARGS:action "@eq 1" \
            "t:none,\
            ctl:ruleRemoveTargetById=931130;ARGS:url,\
            ctl:ruleRemoveTargetById=931130;ARGS:url,\
            ctl:ruleRemoveTargetById=931130;ARGS:facebook,\
            ctl:ruleRemoveTargetById=931130;ARGS:instagram,\
            ctl:ruleRemoveTargetById=931130;ARGS:linkedin,\
            ctl:ruleRemoveTargetById=931130;ARGS:myspace,\
            ctl:ruleRemoveTargetById=931130;ARGS:pinterest,\
            ctl:ruleRemoveTargetById=931130;ARGS:soundcloud,\
            ctl:ruleRemoveTargetById=931130;ARGS:tumblr,\
            ctl:ruleRemoveTargetById=931130;ARGS:youtube,\
            ctl:ruleRemoveTargetById=931130;ARGS:wikipedia,\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:pass1,\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:pass1-text,\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:pass2"

# Create user
SecRule REQUEST_FILENAME "@endsWith /wp-admin/user-new.php" \
    "id:9002540,\
    phase:2,\
    pass,\
    t:none,\
    nolog,\
    ver:'OWASP_CRS/3.2.0',\
    chain"
    SecRule ARGS:action "@streq createuser" \
        "t:none,\
        chain"
        SecRule &ARGS:action "@eq 1" \
            "t:none,\
            ctl:ruleRemoveTargetById=931130;ARGS:url,\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:pass1,\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:pass1-text,\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:pass2"


#
# [ General exclusions ]
#

# _wp_http_referer and wp_http_referer are passed on a lot of wp-admin pages
SecAction \
    "id:9002600,\
    phase:2,\
    pass,\
    t:none,\
    nolog,\
    ctl:ruleRemoveTargetById=920230;ARGS:_wp_http_referer,\
    ctl:ruleRemoveTargetById=931130;ARGS:_wp_http_referer,\
    ctl:ruleRemoveTargetById=932150;ARGS:_wp_http_referer,\
    ctl:ruleRemoveTargetById=941100;ARGS:_wp_http_referer,\
    ctl:ruleRemoveTargetById=942130;ARGS:_wp_http_referer,\
    ctl:ruleRemoveTargetById=942200;ARGS:_wp_http_referer,\
    ctl:ruleRemoveTargetById=942260;ARGS:_wp_http_referer,\
    ctl:ruleRemoveTargetById=942431;ARGS:_wp_http_referer,\
    ctl:ruleRemoveTargetById=942440;ARGS:_wp_http_referer,\
    ctl:ruleRemoveTargetById=920230;ARGS:wp_http_referer,\
    ctl:ruleRemoveTargetById=931130;ARGS:wp_http_referer,\
    ctl:ruleRemoveTargetById=932150;ARGS:wp_http_referer,\
    ctl:ruleRemoveTargetById=941100;ARGS:wp_http_referer,\
    ctl:ruleRemoveTargetById=942130;ARGS:wp_http_referer,\
    ctl:ruleRemoveTargetById=942200;ARGS:wp_http_referer,\
    ctl:ruleRemoveTargetById=942260;ARGS:wp_http_referer,\
    ctl:ruleRemoveTargetById=942431;ARGS:wp_http_referer,\
    ver:'OWASP_CRS/3.2.0'"

#
# [ Content editing ]
#

# Edit posts and pages
# /wp-admin/post.php, /wp-admin/post.php?t=[timestamp]
# - Themes do not properly escape post_title in HTML, so beware of XSS
#   and be conservative in excluding this parameter.
# - Parameter _wp_http_referer can appear multiple times.
SecRule REQUEST_FILENAME "@endsWith /wp-admin/post.php" \
    "id:9002700,\
    phase:2,\
    pass,\
    t:none,\
    nolog,\
    ver:'OWASP_CRS/3.2.0',\
    chain"
    SecRule ARGS:action "@rx ^(?:edit|editpost)$" \
        "t:none,\
        chain"
        SecRule &ARGS:action "@eq 1" \
            "t:none,\
            ctl:ruleRemoveTargetByTag=attack-sqli;ARGS:post_title,\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:content,\
            ctl:ruleRemoveById=920272,\
            ctl:ruleRemoveById=921180"

# Autosave posts and pages
# ARGS_NAMES:data[wp-check-locked-posts][] can appear multiple times
SecRule REQUEST_FILENAME "@endsWith /wp-admin/admin-ajax.php" \
    "id:9002710,\
    phase:2,\
    pass,\
    t:none,\
    nolog,\
    ver:'OWASP_CRS/3.2.0',\
    chain"
    SecRule ARGS:action "@streq heartbeat" \
        "t:none,\
        chain"
        SecRule &ARGS:action "@eq 1" \
            "t:none,\
            ctl:ruleRemoveTargetByTag=attack-sqli;ARGS:data[wp_autosave][post_title],\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:data[wp_autosave][content],\
            ctl:ruleRemoveTargetById=942431;ARGS_NAMES:data[wp-refresh-post-lock][post_id],\
            ctl:ruleRemoveTargetById=942431;ARGS_NAMES:data[wp-refresh-post-lock][lock],\
            ctl:ruleRemoveTargetById=942431;ARGS_NAMES:data[wp-check-locked-posts][],\
            ctl:ruleRemoveById=921180,\
            ctl:ruleRemoveById=920272"

# Edit menus
SecRule REQUEST_FILENAME "@endsWith /wp-admin/nav-menus.php" \
    "id:9002720,\
    phase:2,\
    pass,\
    t:none,\
    nolog,\
    ver:'OWASP_CRS/3.2.0',\
    chain"
    SecRule ARGS:action "@streq update" \
        "t:none,\
        chain"
        SecRule &ARGS:action "@eq 1" \
            "t:none,\
            ctl:ruleRemoveTargetById=942460;ARGS:menu-name,\
            ctl:ruleRemoveTargetById=941330;ARGS:nav-menu-data,\
            ctl:ruleRemoveTargetById=941340;ARGS:nav-menu-data,\
            ctl:ruleRemoveTargetById=942200;ARGS:nav-menu-data,\
            ctl:ruleRemoveTargetById=942260;ARGS:nav-menu-data,\
            ctl:ruleRemoveTargetById=942330;ARGS:nav-menu-data,\
            ctl:ruleRemoveTargetById=942340;ARGS:nav-menu-data,\
            ctl:ruleRemoveTargetById=942430;ARGS:nav-menu-data,\
            ctl:ruleRemoveTargetById=942431;ARGS:nav-menu-data,\
            ctl:ruleRemoveTargetById=942460;ARGS:nav-menu-data"

# Edit text widgets (can contain custom HTML)
SecRule REQUEST_FILENAME "@endsWith /wp-admin/admin-ajax.php" \
    "id:9002730,\
    phase:2,\
    pass,\
    t:none,\
    nolog,\
    ver:'OWASP_CRS/3.2.0',\
    chain"
    SecRule ARGS:action "@rx ^(?:save-widget|update-widget)$" \
        "t:none,\
        chain"
        SecRule &ARGS:action "@eq 1" \
            "t:none,\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:widget-text[0][text],\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:widget-text[1][text],\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:widget-text[2][text],\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:widget-text[3][text],\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:widget-text[4][text],\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:widget-text[5][text],\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:widget-text[6][text],\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:widget-text[7][text],\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:widget-text[8][text],\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:widget-text[9][text],\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:widget-text[10][text],\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:widget-text[11][text],\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:widget-text[12][text],\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:widget-text[13][text],\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:widget-text[14][text],\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:widget-text[15][text],\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:widget-text[16][text],\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:widget-text[17][text],\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:widget-text[18][text],\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:widget-text[19][text],\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:widget-text[20][text],\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:widget-text[21][text],\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:widget-text[22][text],\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:widget-text[23][text],\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:widget-text[24][text],\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:widget-text[25][text],\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:widget-text[26][text],\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:widget-text[27][text],\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:widget-text[28][text],\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:widget-text[29][text],\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:widget-text[30][text],\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:widget-text[31][text],\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:widget-text[32][text],\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:widget-text[33][text],\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:widget-text[34][text],\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:widget-text[35][text],\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:widget-text[36][text],\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:widget-text[37][text],\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:widget-text[38][text],\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:widget-text[39][text]"

# Reorder widgets
SecRule REQUEST_FILENAME "@endsWith /wp-admin/admin-ajax.php" \
    "id:9002740,\
    phase:2,\
    pass,\
    t:none,\
    nolog,\
    ver:'OWASP_CRS/3.2.0',\
    chain"
    SecRule ARGS:action "@streq widgets-order" \
        "t:none,\
        chain"
        SecRule &ARGS:action "@eq 1" \
            "t:none,\
            ctl:ruleRemoveTargetById=942430;ARGS:sidebars[sidebar-1],\
            ctl:ruleRemoveTargetById=942431;ARGS:sidebars[sidebar-1],\
            ctl:ruleRemoveTargetById=942430;ARGS:sidebars[sidebar-2],\
            ctl:ruleRemoveTargetById=942431;ARGS:sidebars[sidebar-2],\
            ctl:ruleRemoveTargetById=942430;ARGS:sidebars[sidebar-3],\
            ctl:ruleRemoveTargetById=942431;ARGS:sidebars[sidebar-3],\
            ctl:ruleRemoveTargetById=942430;ARGS:sidebars[sidebar-4],\
            ctl:ruleRemoveTargetById=942431;ARGS:sidebars[sidebar-4],\
            ctl:ruleRemoveTargetById=942430;ARGS:sidebars[sidebar-5],\
            ctl:ruleRemoveTargetById=942431;ARGS:sidebars[sidebar-5],\
            ctl:ruleRemoveTargetById=942430;ARGS:sidebars[sidebar-6],\
            ctl:ruleRemoveTargetById=942431;ARGS:sidebars[sidebar-6],\
            ctl:ruleRemoveTargetById=942430;ARGS:sidebars[sidebar-7],\
            ctl:ruleRemoveTargetById=942431;ARGS:sidebars[sidebar-7]"

# Create permalink sample for new post
SecRule REQUEST_FILENAME "@endsWith /wp-admin/admin-ajax.php" \
    "id:9002750,\
    phase:2,\
    pass,\
    t:none,\
    nolog,\
    ver:'OWASP_CRS/3.2.0',\
    chain"
    SecRule ARGS:action "@streq sample-permalink" \
        "t:none,\
        chain"
        SecRule &ARGS:action "@eq 1" \
            "t:none,\
            ctl:ruleRemoveTargetByTag=attack-sqli;ARGS:new_title"

# Add external link to menu
SecRule REQUEST_FILENAME "@endsWith /wp-admin/admin-ajax.php" \
    "id:9002760,\
    phase:2,\
    pass,\
    t:none,\
    nolog,\
    ver:'OWASP_CRS/3.2.0',\
    chain"
    SecRule ARGS:action "@streq add-menu-item" \
        "t:none,\
        chain"
        SecRule &ARGS:action "@eq 1" \
            "t:none,\
            ctl:ruleRemoveTargetById=931130;ARGS:menu-item[-1][menu-item-url]"

# Editor: Add Media, Insert Media, Insert into page
SecRule REQUEST_FILENAME "@endsWith /wp-admin/admin-ajax.php" \
    "id:9002770,\
    phase:2,\
    pass,\
    t:none,\
    nolog,\
    ver:'OWASP_CRS/3.2.0',\
    chain"
    SecRule ARGS:action "@streq send-attachment-to-editor" \
        "t:none,\
        chain"
        SecRule &ARGS:action "@eq 1" \
            "t:none,\
            ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:html"


#
# [ Options and Settings ]
#

# Change site URL
SecRule REQUEST_FILENAME "@endsWith /wp-admin/options.php" \
    "id:9002800,\
    phase:2,\
    pass,\
    t:none,\
    nolog,\
    ver:'OWASP_CRS/3.2.0',\
    chain"
    SecRule ARGS:option_page "@streq general" \
        "t:none,\
        chain"
        SecRule &ARGS:option_page "@eq 1" \
            "t:none,\
            chain"
            SecRule ARGS:action "@streq update" \
                "t:none,\
                chain"
                SecRule &ARGS:action "@eq 1" \
                    "t:none,\
                    ctl:ruleRemoveTargetById=931130;ARGS:home,\
                    ctl:ruleRemoveTargetById=931130;ARGS:siteurl"

# Permalink settings
# permalink_structure=/index.php/%year%/%monthnum%/%day%/%postname%/
SecRule REQUEST_FILENAME "@endsWith /wp-admin/options-permalink.php" \
    "id:9002810,\
    phase:2,\
    pass,\
    t:none,\
    nolog,\
    ctl:ruleRemoveTargetById=920230;ARGS:selection,\
    ctl:ruleRemoveTargetById=920272;ARGS:selection,\
    ctl:ruleRemoveTargetById=942431;ARGS:selection,\
    ctl:ruleRemoveTargetById=920230;ARGS:permalink_structure,\
    ctl:ruleRemoveTargetById=920272;ARGS:permalink_structure,\
    ctl:ruleRemoveTargetById=942431;ARGS:permalink_structure,\
    ctl:ruleRemoveTargetById=920272;REQUEST_BODY,\
    ver:'OWASP_CRS/3.2.0'"

# Comments blacklist and moderation list
SecRule REQUEST_FILENAME "@endsWith /wp-admin/options.php" \
    "id:9002820,\
    phase:2,\
    pass,\
    t:none,\
    nolog,\
    ver:'OWASP_CRS/3.2.0',\
    chain"
    SecRule ARGS:option_page "@streq discussion" \
        "t:none,\
        chain"
        SecRule &ARGS:option_page "@eq 1" \
            "t:none,\
            chain"
            SecRule ARGS:action "@streq update" \
                "t:none,\
                chain"
                SecRule &ARGS:action "@eq 1" \
                    "t:none,\
                    ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:blacklist_keys,\
                    ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:moderation_keys"

# Posts/pages overview search
SecRule REQUEST_FILENAME "@endsWith /wp-admin/edit.php" \
    "id:9002830,\
    phase:1,\
    pass,\
    t:none,\
    nolog,\
    ctl:ruleRemoveTargetByTag=OWASP_CRS;ARGS:s,\
    ver:'OWASP_CRS/3.2.0'"


#
# [ Helpers ]
#

# /wp-admin/load-scripts.php?c=0&load%5B%5D=hoverIntent,common,
# admin-bar,wp-ajax-response,jquery-color,wp-lists,quicktags,
# jquery-query,admin-comments,svg-painter,heartbeat,&load%5B%5D=
# wp-auth-check,wp-a11y,wplink,jquery-ui-core,jquery-ui-widget,
# jquery-ui-position,jquery-ui-menu,jquery-ui-autocomplete&ver=4.6.1
#
# /wp-admin/load-styles.php?c=0&dir=ltr&load%5B%5D=dashicons,
# admin-bar,buttons,media-views,common,forms,admin-menu,dashboard,
# list-tables,edit,revisions,media,themes,about,nav-menu&load%5B%5D=
# s,widgets,site-icon,l10n,wp-auth-check&ver=4.6.1
#
# /wp-admin/load-scripts.php?c=0&load%5B%5D=hoverIntent,common,
# admin-bar,jquery-ui-widget,jquery-ui-position,wp-pointer,
# wp-ajax-response,jquery-color,wp-lists,quicktags,
# jqu&load%5B%5D=ery-query,admin-comments,jquery-ui-core,
# jquery-ui-mouse,jquery-ui-sortable,postbox,dashboard,underscore,
# customize-base,customize&load%5B%5D=-loader,thickbox,plugin-install,
# wp-util,wp-a11y,updates,shortcode,media-upload,svg-painter,
# jquery-ui-accordion&ver=3f9999390861a0133beda3ee8acf152e
SecRule REQUEST_FILENAME "@rx /wp-admin/load-(?:scripts|styles)\.php$" \
    "id:9002900,\
    phase:2,\
    pass,\
    t:none,\
    nolog,\
    ctl:ruleRemoveById=921180,\
    ctl:ruleRemoveTargetById=920273;ARGS_NAMES:load[],\
    ctl:ruleRemoveTargetById=942432;ARGS_NAMES:load[],\
    ctl:ruleRemoveTargetById=942360;ARGS:load[],\
    ctl:ruleRemoveTargetById=942430;ARGS:load[],\
    ctl:ruleRemoveTargetById=942431;ARGS:load[],\
    ctl:ruleRemoveTargetById=942432;ARGS:load[],\
    ver:'OWASP_CRS/3.2.0'"


SecMarker "END-WORDPRESS-ADMIN"


SecMarker "END-WORDPRESS"

</IfModule>

Test your site with a malicious URL, like this:

https://xn--1ca.se/hack.php?param=../../etc
https://xn--1ca.se/?q=%22%3E%3Cscript%3Ealert(1)%3C/script%3E%27

Here is a useful one-liner to find false positives from your own server

# cat error.log|grep ModSec|grep <your_ip>|grep -oP '(?<=\[id\s\")\w+'|sort|uniq