Cookie Settings
Customize Consent Preferences

We use cookies to help you navigate efficiently and perform certain functions. You will find detailed information about all cookies under each consent category below.

The cookies that are categorized as "Necessary" are stored on your browser as they are essential for enabling the basic functionalities of the site. ... 

Always Active

Necessary cookies are required to enable the basic features of this site, such as providing secure log-in or adjusting your consent preferences. These cookies do not store any personally identifiable data.

No cookies to display.

Functional cookies help perform certain functionalities like sharing the content of the website on social media platforms, collecting feedback, and other third-party features.

No cookies to display.

Analytical cookies are used to understand how visitors interact with the website. These cookies help provide information on metrics such as the number of visitors, bounce rate, traffic source, etc.

No cookies to display.

Performance cookies are used to understand and analyze the key performance indexes of the website which helps in delivering a better user experience for the visitors.

No cookies to display.

Advertisement cookies are used to provide visitors with customized advertisements based on the pages you visited previously and to analyze the effectiveness of the ad campaigns.

No cookies to display.

Other cookies are those that are being identified and have not been classified into any category as yet.

No cookies to display.

scan

Fixing the case of the Implicit Flow modification


Last year I met this web application. Let’s call it Hank. Hank was pretty, but not that smart. In particular, Hank was very prone to trusting what user’s told it. Earlier we learned about how Hank would just trust what a user entered, and then later use that against others. But that is not what we are talking about today. Today we are talking about how Hank would let a user modify some data and become an administrator.

You see, the browser is a funny world. It tries very hard to protect its user from the threats that lurk on the web. But, it’s also not very trustworthy itself. Any user can modify data in it. You can’t have secrets, you can’t trust fields. You can’t have some variable ‘admin=true’ and expect the user to just behave.

In Hank’s case it used a login method called OpenID Connect. Yes the very same one I’m a big fan of. But, Hank skipped some classes and used the Implicit Flow instead of the PKCE flow. Bad Hank. This would be fine if Hank just used the authentication and passed the token as-is to the back-end, but, Hank used some fields in the ID Token to indicate role (e.g. admin vs end-user). Worse, Hank’s back-end used the same fields but did not validate the ID Token. So a user could login, modify the resulting info, and pass it back to the back-end and become an admin. Naughty Hank!

Now the correct solution here is sometimes called the 3-legged flow. In this model, the front end (the browser), sets up and finishes the login flow, but, the last step is it shares a code w/ the back-end which then goes and fetches from the authentication and retrieves the token. In this model we have a protected code base and can have secrets, we would often cipher the result and make a session cookie, making it impossible to modify in the front-end.
So, how would you solve this? Send Hank in for some code surgery? That would be the best. But what if you don’t have the time or money? Can you let Hank loose on the Internet?

Sure you can! The fix was complex, but, in the Web Application Firewall we intercepted the response to the back-end that had the fields in it that were prone to modification. Using the Access Token that Hank supplied, we then went and fetched the real values (in this example the user role), overwriting what the user supplied. We then ciphered with something the user doesn’t have access to (since its in the back-end), and returned it as a cookie. Later, on other requests, we would translate it back for the back-end.

Fully transparent, fully secure. Now that you’ve read the story of Hank, I hope that you will read your OpenID Code Flows and look closely at PKCE or 3-legged. Don’t allow critical information to be modified in the browser and just trusted in the backend. Trust, but verify. Defense in Depth. Assume the browser is broken. Its just easier that way.

As for the workaround in the Web Application Firewall? First we make the observation that we have already confirmed the validity of the Bearer token. Then, we put in a rule like this:

local auth_header = ngx.req.get_headers()["authorization"]
local str = require "resty.string"
local aes = require "resty.aes"
local ck = require "resty.cookie"
local cookie, err = ck:new()
local aes_256_cbc_sha512x5 = aes:new(session_secret, nil, aes.cipher(256,"cbc"), aes.hash.sha512, 5)
local encrypted = aes_256_cbc_sha512x5:encrypt(auth_header)
-- str.to_hex(encrypted),
local ok, err = cookie:set({
  key = "agilicus_token",
  value = ngx.encode_base64(encrypted, true),
  path = "/",
  httponly = true,
  secure = true,
  samesite = "Strict"
})

Later we use that encrypted cookie overriding what the user can provide. We also do an east-west call (replicating what the browser did and trusted), but in the back-end where they cannot modify:

local http = require "resty.http"
local httpc = http.new()
local auth_header = ngx.req.get_headers()["authorization"]
-- Initialise response to 500, override if we logout
ngx.status = 500
if auth_header then
  ngx.say("Error: cannot log out, Host or Authentication header is not set")
else
  local domain = x.."."..y.."."..z
  local uri = "https://auth."..domain.."/token/revoke"
  local token = string.match(auth_header, "[^ ]+ (.*)")
  local res, err = httpc:request_uri(uri, {
  method = "POST",
  body = "token="..token.."&token_type_hint=access_token",
  headers = {
    ["Content-Type"] = "application/x-www-form-urlencoded",
  },
  keepalive_timeout = 1,
  keepalive_pool = 1
})
if not res then
  ngx.say("Failed to revoke access token on "..uri.." : ", err)
else
  ngx.status = res.status
  ngx.say(res.body)
end
end