ModSecurity returning 406 on OAuth callbacks, and the two wrong answers I found first

Asked in Networking & Security, on The Patch Panel.

ModSecurity returning 406 on OAuth callbacks, and the two wrong answers I found first
 
Notifications
Clear all

ModSecurity returning 406 on OAuth callbacks, and the two wrong answers I found first

1 Posts
1 Users
0 Reactions
94 Views
Posts: 9
Admin
Topic starter
Posh · Waking
(@keh)
Member
Joined: 2 months ago
[#7]

Update, 20 September: solved. It took three support chat sessions, but Bluehost ended up whitelisting the rule itself for this site rather than one parameter at a time. How it ended is at the bottom.

Spent today getting Google sign-in working on this site. The fix was one line from a support agent in the end, but the road there is worth writing down, because I got it wrong twice and both mistakes are ones I have seen other people make.

The symptom. Click “Continue with Google”, go through Google, land back on the site, and get:

Not Acceptable!
An appropriate representation of the requested resource could not be found on this server. This error was generated by Mod_Security.

Shared hosting, Apache, cPanel. No access to the ModSecurity config, no access to the audit log.

Wrong answer number one: “wp-login.php is blocked”

I probed from my workstation. wp-login.php returned 406. Bare, with parameters, with a browser user agent, on GET and on HEAD. Consistent every time. I concluded the login page was blocked for everyone and that nobody could sign in at all.

That was wrong, and the method was wrong. ModSecurity blocks a source address for a while after that address trips a rule, and a burst of probing is exactly how you trip one. I had spent the morning crawling the site and firing test URLs at it. I was not measuring the site. I was measuring what the site now thought of me.

The test that settled it: run the same request from a second address. I used the server’s own IP over SSH. wp-login.php returned 200 from there at the same moment it was returning 406 from my desk.

If you take one thing from this: never diagnose a WAF from a single source address, and be suspicious of any result you got after hammering it.

The actual rule

Probing properly, from an address that had been quiet, the trigger is a URL scheme followed by // in any query-string value:

406  /?foo=https%3A%2F%2Fexample.com
406  /?foo=https%3A%2F%2F
406  /?foo=http%3A%2F%2Fexample.com
200  /?foo=https%3A            (scheme, no encoded slashes)
200  /?foo=%2F%2Fexample.com   (slashes, no scheme)

Path-independent: the same on /, on wp-login.php and on a subfolder. It is the signature of a remote-file-inclusion rule, and on a shared host you will not be told which one.

That matters for OAuth because the callback carries the granted scopes as encoded URLs. You cannot change the format. It is the provider’s.

Wrong answer number two: testing a URL I made up

Support whitelisted the scope parameter. I rebuilt what I thought the callback looked like, tested it, got 200, and called it fixed.

It was not fixed. The next real sign-in returned the same 406.

My reconstruction was missing a parameter. Google also sends iss=https%3A%2F%2Faccounts.google.com, the issuer, and I had not included it because I had never looked at a real callback — only at what I assumed one contained.

What actually solved it

The raw access log, which on cPanel shared hosting is readable at ~/access-logs/ even when the ModSecurity audit log is not. The real failed request was sitting there with its status code and every parameter:

GET /wp-login.php?loginSocial=google&state=...
    &iss=https%3A%2F%2Faccounts.google.com
    &code=...&scope=...&authuser=0&prompt=none   406

Replaying that exact URL: 406. The same URL with only &iss=... removed: 200. One parameter, proven in two requests, no guessing.

Go to the access log first. I lost an hour to reconstructions that were subtly wrong when the real request was on disk the whole time.

The bit worth arguing about

Support fixed it by whitelisting parameter names — first scope, then iss — rather than scoping the rule to the domain. That works until the next parameter. ?state= still returns 406 today, and plenty of providers put a return URL in state.

I asked for a per-domain exclusion and was told it was not available on this plan. (It turned out to be available after all: see below.) I do not know whether that is a real constraint or a first-line script.

So — has anyone got a per-domain ModSecurity exclusion out of a shared host, and what did you have to say to get there? And has anyone found a way to see the audit log on cPanel shared hosting without opening a ticket for every rule ID? That is the part that made this slow.

How it ended

It took three support chat sessions with Bluehost. The first fixes were the parameter-by-parameter whitelists described above: scope, then iss. The one that finished it whitelisted the rule itself for keh-tech.net. The rule ID is 340163, which is a vendor rule in the 340xxx range, not an OWASP CRS rule, so my remote-file-inclusion guess was right about the mechanism and wrong about where the rule came from.

Tested on 21 September from a quiet address:

302  /wp-login.php?loginSocial=google&iss=https%3A%2F%2Faccounts.google.com
200  /wp-login.php?state=https%3A%2F%2Fexample.com
200  /?foo=https%3A%2F%2Fexample.com      (the reproducer that used to 406)

406  /?id=1 UNION SELECT ...              (SQL injection)
406  /?q=<script>...                      (cross-site scripting)
406  /?file=../../../../etc/passwd        (path traversal)

So every URL-valued parameter gets through now, state included, while the rest of ModSecurity is still doing its job. Only the one rule that could not tell an OAuth callback from an attack is off.

If you are on shared hosting with the same problem, what worked here was:

  • Find the real request in ~/access-logs/ and prove which parameter trips it, in two requests.
  • Ask for the rule ID, and ask for that rule to be excluded for your domain. Say plainly that you do not want ModSecurity switched off for the domain, or you may be offered exactly that.
  • Keep the rule ID on record. If the block comes back after a ruleset update, quoting it saves starting over.
  • Expect more than one chat. The first answer is often a whitelist for one parameter. If a different parameter still fails, go back and ask again for the rule, not the parameter.

The audit-log question above still stands: I never did find a way to read the ModSecurity audit log on cPanel shared hosting without asking support.


Share: