403 XMLRPC Error In WordPress Hosted On Windows IIS (Remember Your Security Changes)

This is more of a note for myself.  Hopefully it will end up in a search engine and I’ll be able to find my own solution when this happens again.

Yesterday, I went to post something to one of my blogs that I self-host on a Windows server.  You’re on one of those blogs right now, go figure.  And when I did, I got a very common error: 403 Forbidden when accessing the xmlrpc.php file.  But, I had just successfully posted two days ago!  What could have changed?

I connected to the server and updated the permissions on the folder, since that is usually what 403 means.  That didn’t fix it.  I did an IISRESET.  That didn’t fix it.  I did a full reboot.  That didn’t fix it.  I kept searching for information.

When you search for WordPress and xmlrpc, you get a crap-ton of results that warn of the dangers of leaving this file exposed.  It’s a gateway for hackers!!!!!!  It’s so hackable!!!!!  Turn it off!!!!  Disable any remote capabilities to WordPress!!!!  These warning are geared toward users who use and reuse weak passwords.  Of course there is authentication on anything xmlrpc does, so if you’re getting hacked, it’s your fault.  Granted, there’s no protection from a hacker brute-forcing your xmlrpc URL until it finally hits something, but again, strong, unique passwords, people!

With that little rant out of the way, I do recognize the high sensitivity of this file.  It would not be unlike me to take some extra precautions on it.  And that’s exactly what I had done, and what I had forgotten I’d done.

I set up an IP address restriction on just that file, so only my home PC could access it.  That makes sense since I only post using Live Writer from home.  However, my IP address from my ISP changed in the last couple days and I didn’t notice.  This is how I fixed it.  If you instead want to use this info to secure your own WP site (and forget about it until your IP changes), go ahead.

In IIS, I navigated to the xmlrpc.php file in the content view

Annotation 2020-04-19 111349

After highlighting the file, I right-clicked and chose Switch to Features View.  You can see the specific file name is in the title.  I chose IP Address Restrictions

Annotation 2020-04-19 111727

And in here is where I added/changed my home IP address.  You can find yours at https://www.whatismyip.com.

Annotation 2020-04-19 112011

And as you would expect, I am able to post to blogs again, as evidenced by this post right here.

Yahoo Security Is Still A Joke

Today I got an email from Yahoo.  You know, they’ve been having some security issues lately with millions of accounts being compromised.

The email said they noticed I hadn’t changed my password in a while.  That was kind of odd since I thought I got this email not long ago and humored them by changing my password.  I don’t use Yahoo for anything important anymore, so I didn’t really care.


When I looked at where the email was sent to, it was sent to a non-yahoo email address.  It was sent to an email address I used to log in to Facebook.  Putting it together, this was an account I used to log in to Flickr using the Facebook account login option.  It’s been a while since Yahoo gave the middle finger to external logins because they didn’t want to support them anymore.  But that doesn’t mean they cleared all that data out.

So let’s get this right.  I used to log in to Flickr using a Facebook login.  Yahoo discontinued Facebook login ability.  I can’t remember if I converted to a Yahoo account or abandoned it, but regardless, there is an account in Yahoo’s system that has a Facebook email in it.  I can’t find that email anywhere in my password manager.  Even if I did, there’s no Yahoo password for me to change.  It’s a Facebook login.

I used to be pretty neutral on Yahoo.  I didn’t care one way or the other about them.  That’s changed.  I really want them to close up shop.  They are not doing the world any favors with their lack of security and perpetually changing services.

Two-Factor Authentication Primer

I recently implemented two-factor authentication into a web app and since it was a new concept for me, I thought it would be good to explain the highest conceptual level of this process.  As with a lot of new things, there’s some terminology to learn and there’s a need to understand how all the pieces fit together.

First, what’s it take to integrate this with an existing profile login?  You need a new database field and a bit of extra code for opting in and out of the two-factor authentication.  Ideally, you’ll need a library for generating a QR code, too.

Before I get too much into it, these are some of the elements of the process.  There are three pieces of data involved:

  • Shared Secret: This element is stored in the database with the user profile and is never exposed outside your application.
  • Secret Key: This is an encoded version of the Shared Secret.  It is given to the user by your application and the user enters it into their authenticator application.
  • Code: The numeric value generated by the authenticator application.  This changes every minute.

In brief, your application and the authenticator application both use the current time plus the Secret Key to generate a Code.  If they match, the user is authenticated.

To implement this, you would modify your user profile page to provide a button to enable two-factor.  When the button is clicked, you create a a random Shared Secret and save it to their profile.  You use that Shared Secret to generate and return the Secret Key.  The user puts that Secret Key in their authenticator app and the opt-in is complete.

When the user logs in to your application, if they have a Shared Secret set in their profile, they are prompted to enter the Code from their authenticator app.  Your application compares that Code to the Code it generates itself, using the Secret Key (built from the Shared Secret).  If it is the same, the user is logged in.

It really is simple.  The only thing that isn’t clear, but can be found with some moderate Internet searching is the URL to embed in the QR code.  That URL is: otpauth://totp/{0}?secret={1}, where {0} is the name of the profile to use (either your application or the user’s username or both) and {1} is the Secret Key.  Authenticator apps allow manual entry of Secret Keys, so if you don’t provide a QR code, it’s still workable, just a bit tedious.

Some of the other pieces you’d need are functions to reset the Shared Secret or clear it, if the user wanted to opt-out.  This is simple user account maintenance.  With a simple implementation, you could blank out the Shared Secret on a “forgot password” action.  With more sensitive data, you may want a second code to allow a password reset.  The big concern is users who have lost their phone or wiped out their authenticator application entries.

Because two-factor authentication is so simple and is such a low-impact to existing user profile data structures (relative to oAuth), plus the fact it can be opt-in, it’s really a no-brainer to add it to your applications.

IsInRole While Disconnected; No Longer IsInHole

In an application I am continuing to write, during startup, the user’s security is determined by the user’s security groups and permissions are granted within the program based on the group membership.  This has worked fine.  Then one day in an airport I wanted to work on some documentation and I quickly discovered that I could not run the program.  I could not run the program because I was not on the VPN and the Active Directory (AD) groups could not be enumerated to check my permissions.  At the time, I was kind of grateful I couldn’t do any work and didn’t give it much more thought. Our application is not designed to be run in a disconnected fashion.  You have to be on the VPN to get to the database server and use it.  Lately, I revisited the problem and decided to resolve it.

In my particular instance, I had a database running locally, but I was missing an Active Directory server to read from.  I had my cached credentials, shouldn’t that be enough?  Well, it is, if you do it the hard way.

Everyone should know that a user or a group is just a name.  There is an ID behind that group or user, which allows you to rename the group/user without breaking anything.  Behind the scenes, Windows always uses the ID.  The name is just for your benefit.

Using the excellent tool WhoAmI, I saw the following (don’t chide me for running as Administrator):

C:\>whoami /groups /sid

[Group  1] = "700CB\Domain Users"  S-1-5-21-2454202000-1896829455-2950045386-513
[Group  2] = "Everyone"  S-1-1-0
[Group  3] = "DA3\Debugger Users"  S-1-5-21-854245398-1547161642-725345543-1007
[Group  4] = "DA3\Offer Remote Assistance Helpers"  S-1-5-21-854245398-1547161642-725345543-1004
[Group  5] = "BUILTIN\Users"  S-1-5-32-545
[Group  6] = "BUILTIN\Administrators"  S-1-5-32-544
[Group  7] = "NT AUTHORITY\INTERACTIVE"  S-1-5-4
[Group  8] = "NT AUTHORITY\Authenticated Users"  S-1-5-11
[Group  9] = "LOCAL"  S-1-2-0
[Group 10] = "700CB\Domain Admins"  S-1-5-21-2454202000-1896829455-2950045386-512

When disconnected form the LAN (you might need to reboot while disconnected to clear the cache), it looks like:

C:\>whoami /groups /sid

[Group  1] = ""  S-1-5-21-2454202000-1896829455-2950045386-513
[Group  2] = "Everyone"  S-1-1-0
[Group  3] = "DA3\Debugger Users"  S-1-5-21-854245398-1547161642-725345543-1007
[Group  4] = "DA3\Offer Remote Assistance Helpers"  S-1-5-21-854245398-1547161642-725345543-1004
[Group  5] = "BUILTIN\Users"  S-1-5-32-545
[Group  6] = "BUILTIN\Administrators"  S-1-5-32-544
[Group  7] = "NT AUTHORITY\INTERACTIVE"  S-1-5-4
[Group  8] = "NT AUTHORITY\Authenticated Users"  S-1-5-11
[Group  9] = "LOCAL"  S-1-2-0
[Group 10] = ""  S-1-5-21-2454202000-1896829455-2950045386-512

My local group names were resolved to their names, but my domain group names couldn’t resolve because AD was unreachable.  My cached profile still had the SIDs though.

So, if I could do an IsInRole check using the SID instead of the domain group name, I’d be golden.  And this is just what I did.

Imports System.Security.Principal
Imports System.Threading

Dim sid As New SecurityIdentifier("S-1-5-21-1859785585-1835888107-1082013118-1025")
Dim p As WindowsPrincipal = CType(Thread.CurrentPrincipal, WindowsPrincipal)


So I took the SID for the groups I was testing for and tested for them instead.  Obviously, if an admin deleted and recreated the group thinking nobody would notice, it would be hell to troubleshoot, so if you’re not fully in control of your environment, you might want to steer clear.  Maybe do the SID after checking IsNetworkAvailable to reduce the exposure to failure?

But for me, it works like a champ, and now I can work in airports.  Hmmm.  Why did I figure this out again?