Protecting WordPress based websites

P

WordPress is one of the most popular web publishing software, both in the private and commercial sectors. While the private sector will hardly use a Citrix NetScaler ADC, not to mention, Citrix Firewall, it is rather common in the commercial world.

This page will focus on a simple, robust deployment. It requires advanced (enterprise) or premium (platinum) editions of Citrix NetScaler ADC. It’s not extra secure, but way more secure than without ADC. Getting a 100 % secure deployment would take way more than just a single blog, this blog is intended to be a good point to start. It’s up to you to increase security by adding more WAF features, BOT-Management and many more.


The basic setup

We need the following features enabled:

  • load-balancing (LB)
  • content-switching (CS)
  • authentication (AAA)
  • web application firewall (AppFw) or responder

publishing WordPress pages using Citrix NetScaler ADCWe will publish a content-switching vServer of type SSL to the internet. There will be two load-balancing vServers, one for normal traffic, and one for administration. To be able to connect to the administration vServer, you need to authenticate first. The default vServers needs its own WAF policy and its own WAF profile, the admin vServer won’t be protected.

Authentication

I won’t go into authentication here. You may use local authentication, or authenticate to an external source like LDAP, RADIUS, SAML or OpenID Connect. You might bind an n-factor flow to it of course. My setup does not utilize single sign-on (SSO), so there will always be at least 2 authentication steps: The first authentication to the Citrix NetScaler, and the second one to WordPress. Why do I not use 2-factor plugins for WordPress like Two-Factor or WP 2FA? First, I don’t trust WordPress as a whole, and second, any added plugin is an additional security risk. In opposite to this, a Citrix NetScaler is a pretty secure appliance, so it actually increases security. You might argue, we already had security issues with Citrix NetScaler (Shitrix, …). That’s true, however, there is still WordPress authentication left in case of authentication to the ADC is broken.

Citrix NetScaler ADC authentication server for the wordpress

add authentication vserver aaa_vs_blog SSL 0.0.0.0 -maxLoginAttempts 3 -failedLoginTimeout 5 (The authentication vServer is limited to 3 login attempts within 5 minutes)
bind authentication vserver aaa_vs_blog -policy aaa_pol_ldap_norz -priority 100 -gotoPriorityExpression NEXT
bind ssl vserver aaa_vs_blog -certkeyName norz.at

 

The admin load-balancer

The only reason for an extra admin load-balancer is the possibility to bind a AAA vServer to it. It points to one or more web servers. Similar to the AAA vServer, it’s nonaddressable. The admin load-balancer will usually not get a WAF-policy bound to it, as it is used by trusted users only.

Authenticated users won’t use the default vServer anymore, they will always use the admin load-balancer only, as they own a valid NSC_TMAS cookie. That’s an important thing to keep in mind: WAF gets bypassed as soon as you are authenticated!

Citrix NetScaler ADC load-balancing server for the wordpress admin page

add lb vserver lb_vs_admin SSL 0.0.0.0 0 -persistenceType COOKIEINSERT -timeout 60 -cltTimeout 180
bind lb vserver lb_vs_admin sc_norz.at_ssl
bind ssl vserver aaa_vs_blog -certkeyName norz.at
(replace norz.at with the domain you’re using!)

The default web-server

It’s just a normal nonaddressable load-balancing vServer, an exact copy of the admin load-balancer.  It will get a Citrix WAF policy bound to it.

Citrix NetScaler ADC load-balancing server for wordpress

add lb vserver lb_vs_web SSL 0.0.0.0 0 -persistenceType COOKIEINSERT -timeout 60 -cltTimeout 180
bind lb vserver lb_vs_web sc_norz.at_ssl
bind ssl vserver aaa_vs_blog -certkeyName norz.at

The content-switching vServer

… ist der Einstiegspunkt zu unserer Lösung. Also adressierbar natürlich. Es werden zwei Richtlinien daran gebunden, eine für die Authentifizierung und eine für den Admin-Lastausgleichs-vServer. Der Standard-vServer wird als Standard für den Contentswitching-vServer festgelegt.

Citrix NetScaler ADC content-switching server for the wordpress

add cs vserver cs_vsrv_www SSL 192.168.229.200 443 -cltTimeout 120
bind cs vserver cs_vsrv_www -lbvserver lb_vs_web
bind ssl vserver cs_vsrv_www -certkeyName norz.at
set ssl vserver cs_vsrv_www -sslProfile ns_default_ssl_profile_secure_frontend

Content-switching policies

Following Citrix’s leading practices, I create content-switching actions instead of binding vServers to the policies. It’s a matter of taste, but Citrix might change the product later on, so I want to be safe.

Citrix NetScaler ADC contentswitching action cs-policy for the authentication vServer

add cs action cs_act_admin -targetLBVserver lb_vs_admin (always use content-switching actions!)
add cs action cs_act_aaa -targetVserver aaa_vs_blog
add cs policy cs_pol_aaa -rule "HTTP.REQ.HOSTNAME.EQ(\"aaa.norz.at\")" -action cs_pol_aaa
add cs policy cs_pol_admin -rule "HTTP.REQ.HOSTNAME.EQ(\"norz.at\") && (HTTP.REQ.COOKIE.CONTAINS(\"NSC_TMAS\") || HTTP.REQ.URL.CONTAINS(\"wp-admin\") || HTTP.REQ.URL.CONTAINS(\"/cgi/selfauth\") ) " -action cs_act_admin

The last policy is a bit harder to understand. It’s case-sensitive, which is OK, as normal users usually don’t add URLs manually. It will select the admin webserver, in case the domain name is right and at least one of the following conditions is true:

  • The NSC_TMAS cookie is present, which means, the user had previously been able to authenticate successfully.
  • the URL contains wp-admin (meaning: someone wants to access the admin area)
  • the URL contains /cgi/selfauth. This URL is used by the NetScaler AAA process. Authenticated connections there will set the NSC_TMAS cookie.

bind cs vserver cs_vsrv_www -policyName cs_pol_aaa -priority 100
bind cs vserver cs_vsrv_www -policyName cs_pol_admin -priority 110

If you host more than just one site on the same cs-vServer, you will have to bind the authentication- and admin-policy first, and the default policy last. Otherwise, you would set the default vServer to the default web-server.


A non-WAF setup

Even if you don’t use WAF, you could limit access to files in your WordPress deployment. Many plugins don’t need to be available from outside, so you could block access to these. Most of the designs installed are not used, so you could block access to these. I’ll use the most simple approach: I’ll block all access and just explicitly allow need es URLs.

Responder policies

The first of these responder policies I create is the one you bind with the lowest priority. It blocks everything and is overridden by policies that explicitly allow individual URLs:

set audit syslogParams -userDefinedAuditlog YES allow user-defined log messages
add audit messageaction mg_act_url WARNING "\"denied_access to \" + HTTP.REQ.URL.PATH_AND_QUERY"
add responder action rs_act_redir_home redirect "\"/\"" -responseStatusCode 301
add responder policy rs_pol_block-all true rs_act_redir_home -logAction mg_act_url
bind lb vserver lb_vs_colors -policyName rs_pol_block-all -priority 65536

Next, we will have to create exceptions, meaning: Policies to allow access to certain URLs. Some of them are obvious:

  • add responder policy rs_pol_allow-fonts 'HTTP.REQ.URL.SUFFIX.CONTAINS("woff")' NOOP allows access to fonts of type WOFF and WOFF2.
  • add responder policy rs_pol_allow-postings "HTTP.REQ.URL.PATH.EQ(\"/\") && HTTP.REQ.URL.QUERY.REGEX_MATCH(re/p=\\d{1,6}/)" NOOP allows access to postings in ?p=123 format, at least 1 digit, maximum 6 digits. You’ll have to change the REGEX in case you use a different format.
  • add responder policy rs_pol_allow-tags "HTTP.REQ.URL.PATH.EQ(\"/\") && HTTP.REQ.URL.QUERY.REGEX_MATCH(re/tag=\\w{1,64}/)" NOOP allows access to tags in ?tag=Waf format, at least 1 character, maximum 64 characters.
  • add responder policy rs_pol_allow-feeds "HTTP.REQ.URL.PATH.EQ(\"/\") && HTTP.REQ.URL.QUERY.REGEX_MATCH(re/feed=rss\\d&p=\\d{1,6}/)" NOOP
     allows access to feeds in ?feed=rss2&p=123 format, at least 1 digit, maximum 6 digits.
  • add responder policy rs_pol_allow-category "HTTP.REQ.URL.PATH.EQ(\"/\") && HTTP.REQ.URL.QUERY.REGEX_MATCH(re/cat=\\d{1,6}/)" NOOP
     allows access to categories in ?cat=123 format, at least 1 digit, maximum 6 digits.
  • add responder policy rs_pol_allow-images "HTTP.REQ.URL.PATH.REGEX_MATCH(re~/wp\\-content/uploads/20\\d\\d/\\d\\d\\/[\\w\\-\\.]{1,60}\\.(gif|png|jpg)~)" NOOP access to all images (gif,png,jpg) uploaded between 2000/01 … 2099/12
  • add responder policy rs_pol_allow-default "HTTP.REQ.URL.PATH.EQ(\"/\")" NOOP access to the start page.
  • add responder policy rs_pol_favicon "HTTP.REQ.URL.EQ(\"/robots.txt\")" NOOP access to the robots.txt file.

Additional policies

You’ll probably have to add additional responder policies. All URLs, that are not white-listed yet, will be blocked and get logged into the event log. That’s what I created the logging action (mg_act_url) for. You can use the following command to find URLs still blocked:

shell
tail -F /var/log/ns.log | grep denied_access

You may create policies similar to the ones I mentioned above, possibly based on REGEX to cover plenty of different URLs.


A WAF setup

First, you’ll have to set up signatures. There are plenty of signatures for WordPress built into NetScaler’s Snort signatures. Unfortunately, these signatures are just partly in the web-wordpress section. Some are in the web-misc section as well. The rest of the signatures you’ll need, are depending on your webserver’s OS.

Next, you’ll have to create a WAF profile. Type should be HTML and XML.

add appfw profile appfw_prof_blog -startURLAction block learn log stats -contentTypeAction learn log stats -startURLClosure ON -RefererHeaderCheck if_present -cookieHijackingAction log stats -cookieProxying sessionOnly -fieldConsistencyAction learn log stats -CSRFtagAction learn log stats -crossSiteScriptingAction learn log stats -crossSiteScriptingTransformUnsafeHTML ON -SQLInjectionAction learn log stats -SQLInjectionGrammar ON -SQLInjectionTransformSpecialChars ON -fieldFormatAction learn log stats -bufferOverflowMaxQueryLength 1024 -bufferOverflowMaxTotalHeaderLength 24820 -responseContentType "application/octet-stream" -XMLDoSAction learn log stats -XMLFormatAction log stats -XMLSQLInjectionAction none -XMLXSSAction learn log stats -XMLWSIAction learn log stats -XMLAttachmentAction learn log stats -XMLValidationAction log stats -signatures centos -XMLSOAPFaultAction log stats -type HTML XML -fileUploadTypesAction log stats -insertCookieSameSiteAttribute ON

Deny URLs

  • bind appfw profile appfw_prof_blog_neu -denyURL "/wp-admin" denies access to wp-admin. This one is not that necessary, just to make sure
  • bind appfw profile appfw_prof_blog_neu -denyURL "/wp-cron" I don’t trust wp-cron. I trigger it using a cron job. You may remove this one
  • bind appfw profile appfw_prof_blog_neu -denyURL "/readme.html"completely unnecessary file, located in the root directory.
  • bind appfw profile appfw_prof_blog_neu -denyURL "/wp-config" there are plenty of wp-config files. They contain SQL database users and many more. They must not be available from outside
  • bind appfw profile appfw_prof_blog_neu -denyURL "/wp-links-opml.php"I don’t need access to this file, so I decided to block it.
  • bind appfw profile appfw_prof_blog_neu -denyURL "/wp-settings.php" don’t need access to this file, so I decided to block it.
  • bind appfw profile appfw_prof_blog_neu -denyURL "/license.txt"completely unnecessary file, located in the root directory.
  • bind appfw profile appfw_prof_blog_neu -denyURL "/wp-activate.php"don’t need access to this file, so I decided to block it.
  • bind appfw profile appfw_prof_blog_neu -denyURL "/wp-load.php"don’t need access to this file, so I decided to block it.
  • bind appfw profile appfw_prof_blog_neu -denyURL "/wp-mail.php"don’t need access to this file, so I decided to block it.
  • bind appfw profile appfw_prof_blog_neu -denyURL "/wp-signup.php"don’t need access to this file, so I decided to block it.
  • bind appfw profile appfw_prof_blog_neu -denyURL "/xmlrpc.php"don’t need access to this file, so I decided to block it.

Allow URLs

  • bind appfw profile appfw_prof_blog_neu -startURL "^https://norz\\.at/wp\\-content/uploads/20\\d\\d/\\d\\d\\/[\\w\\-\\.]{1,60}\\.(gif|png|jpg)$" -comment "allow all images in uploads"
  • bind appfw profile appfw_prof_blog_neu -startURL "^https://norz\\.at/\\?page_id=\\d{1,5}$" -comment "access to pages"
  • bind appfw profile appfw_prof_blog_neu -startURL "^https://norz\\.at/favicon\\.ico$" -comment "allow favicon"
  • bind appfw profile appfw_prof_blog_neu -startURL "^https://norz\\.at/\\?p=\\d{1,6}$" -comment "allow pages"
  • bind appfw profile appfw_prof_blog_neu -startURL "^https://norz\\.at/$" -comment "the root document"
  • bind appfw profile appfw_prof_blog_neu -startURL "^https://norz\\.at/robots\\.txt$" -comment "allwo robots.txt"
  • bind appfw profile appfw_prof_blog_neu -startURL "^https://norz\\.at/\\?tag=[\\w\\-]{1,26}$" -comment "allow tags"
  • bind appfw profile appfw_prof_blog_neu -startURL "^https://norz\\.at/\\?feed=rss\\d&p=\\d{1,6}$" -comment "RSS Feeds"
  • bind appfw profile appfw_prof_blog_neu -startURL "^https://norz\\.at/\\?cat=\\d{1,6}$" -comment "Category View"
  • bind appfw profile appfw_prof_blog_neu -startURL "^https://norz\\.at/wp\\-content/(themes|plugins)/[\\w\\-]{3,60}/assets/fonts/[\\w\\-]{3,60}\\.woff" -comment "Access to fonts"

You will need some more for sure. Use the following commands to find out which ones:

shell
tail -F /var/log/ns.log | grep APPFW_STARTURL

HTML Cross-Site Scripting

Normally, I’d say, XSS should get blocked. My blog, however, is a technical blog, mostly viewed by web-server guys. Therefore, it is rather likely for someone to post scripts on my website. So I set XSS to transform. That way, the HTML fragment containing <script> will get transformed into &lt;script&gt;, and it can’t be harmful anymore.

HTML SQL Injection

Similar to HTML Cross-Site Scripting, SQL Injection alike code may very well appear in users’ comments. I also render these harmless. I use the new SQL grammar checks to keep the number of “false positives” as few as possible.

Other security scans

I won’t go into other security scans. I’ll skip these here. Your environment will be pretty much more secure if you follow my guide until here.


Other functions you could potentially use

  • There is BOT management. BOT management allows distinguishing between “good” and “bad” bots. Good bots would be search engines like Ecosia, Yandex, Baidu, Google and many more. Bad bots would be bots harvesting data like mail addresses or job offers or even more malicious ones designed to exploit weaknesses of your deployment.
  • You could use IP reputation service to block machines of bad reputation from connecting to the admin vServer. Same, you might use it to keep these machines from posting comments or sending messages.

As every time: I am happy about suggestions and comments. Please write to me about what comes to your mind!

About the author

Johannes Norz

Johannes Norz is a Citrix Certified Citrix Technology Advocate (CTA), Citrix Certified Instructor (CCI) and Citrix Certified Expert on Application Delivery and Security (CCE-AppDS).

He frequently works for Citrix international Consulting Services and several education centres all around the globe.

Johannes lives in Austria. He had been borne in Innsbruck, a small city (150.000 inhabitants) in the middle of the most beautiful Austrian mountains (https://www.youtube.com/watch?v=UvdF145Lf2I)

Add comment

By Johannes Norz

Recent Posts

Recent Comments