The first thing we need to establish is that I am NOT any kind of "security expert". If you've been programming for long enough, you can't help but be exposed to security issues. Solid programmers should always be thinking about potential security loopholes. And eventually, you touch enough security-related bugs that you end up thinking that you know something about the field.
But the simple fact is that security is its own unique field in IT. For good reason. There's sooooo much to learn with regard to security that it's truly its own discipline. You can't be, say, the best React dev and the best security expert. At some point... you have to choose.
So, with those caveats out of the way, I still think we need to have a little "security chat". Because I've seen some things, in just the last year-or-two of dev, that have made me really nervous. And those scary things have been centered on APIs.
Not Always A Net-Good
If you've ever been tasked with integrating with some third-party system, the first thing you probably asked was, "Where's the API?" In the worst-case scenario, there was no API. But far too often, there was an API... sorta. There were a few exposed endpoints / methods / whatever. There were a few ways to do basic tasks. But when you reached the point of trying to build true integration, all too often, the "API" was sorely lacking.
For this reason, APIs usually fall into three distinct categories:
- There is NO API. And we curse the programmers who originally created this monstrosity.
- There is... some sorta API. It allows you to do some things. But it also provides no interface for most of the critical features we need to implement. And we curse the programmers who originally created this monstrosity.
- There is a true, "full" API. Most-or-all of the features we must implement are already accounted for in the API. And we rejoice in the streets.
But I'm here to tell you: Option #3 isn't always the panacea that it appears to be on the surface. We get so frustrated by systems that lack APIs, or by systems with insufficient APIs, that we assume anything resembling a "full" API must be, unquestionably, an undeniable good.
Common Pitfalls
I've run into some pitfalls lately that make me very nervous about the systems that our institutions are putting into place. I'm gonna reiterate that I'm NOT a security expert. And I know that security experts could probably extend this article by 10,000 words (or more). But even from my quaint little perch as someone who's "just" a programmer, here are some of the headaches that I've been battling recently:
Manual Updating of Derived Values
Let's imagine that you're writing a football ("soccer", for us Americans) management system. And in that system, you can track a goalkeeper's save rate. The "save rate" should be a simple formula that's derived from saves / shotsOnGoal
.
But then imagine that you've created the following endpoint in your API:
PUT /v1/player/{playerId}
And finally, let's assume that this endpoint accepts a player
object that includes the 'save rate' value. Do you see the problem with that logic?? save rate
shouldn't be manually set. It should be calculated, based on that player's total number of saves, divided by that player's total shots on goal. But I've seen too many APIs where I'm given an endpoint that allows me to update "save rate" directly.
Maybe that's a bit too esoteric (or farcical) for you. So allow me to give you a more real-life example:
Last year, I did security consulting for a bank - a bank with physical locations - all throughout Texas. I was specifically helping them with their physical access system. In other words, I was working on the software that would determine when a particular door was open, or who was allowed to go through that door. I think you'll agree with me that such things are quite important in a bank, right??
I was working with a vendor's software that allowed you to create an infinite number of rules that would, ultimately, determine whether a door was open at a given time, or whether a given person was allowed through at that time. But the problem I ran into was that, no matter how much time I spent crafting those rules, there was still a REST endpoint that would manually override ALL of those rules with a simple POST/PUT. Even if every rule in the system said, "LOCK THIS DOOR!!!", you could still do a PUT
to the proper endpoint that would unlock that door, disregarding every other rule in the system.
For some, the knee-jerk reaction is to say:
Well... If you had the required API key to update that door's status, then you should be able to unlock that door, whenever you want.
But this (simplistic) mindset overlooks the fact that the vendor's base system was intricately designed to force you to manage all of the appropriate rules. Except... you could bypass the whole system if you just managed to get ahold of the right key.
Many programmers would scoff at such a "weakness". They'd claim that it's not their problem if the key falls into the wrong hands.
But I assure you that the client (the bank in Texas) was not satisfied with such an answer. In fact, I illustrated for them how I could snag their API key - in a mere matter of hours - and proceed to open every door in every one of their locations. Suffice it to say that they were not thrilled.
The point here is not that you can't expose critical functions in your API. But if some value should only ever be calculated through a separate series of rules, then don't expose a method that will allow any-old-programmer to set that value manually, irrespective of those rules.
API Users With God Rights
I wish I was joking - but I've seen this scenario too many times: The application has an intricate set of roles / rights / permissions defined on the front end. But if you manage to get an API key, that key always comes with god rights.
Think about that. Your app creates finely-delineated rights on the front end. But once someone gets an API key to interact with it on the back end, they can do whatever the hell they want.
I'm not trying to claim that you should never allow an API user with "god rights". But that should never be the default. And if the app provides fine-grained user controls on the front end, you should almost always be accounting for the same thing on the back end.
I saw this recently with a hardware compliance management system. The client had a very pressing need to illustrate that all of their users were compliant with government regulations. But their API afforded only one access method - access via "god rights". This meant that anyone tasked with building the system also had all of the rights they needed to hide their own non-compliance (if they so chose).
Leaky Interfaces
I also recently found a scenario where the API seemed to be fairly "robust" - except...
By using the API, I was able to infer critical details about the underlying database that stored the API's values. In other words, accessing the API gave me the critical info that I needed to hack directly into the database.
Obviously, if you've given me the data I need to crack your underlying data store, then any "security" you slap onto the API layer is rather pointless. With this data in-hand, I actually illustrated for a client how I could change all of their base data without ever officially hacking the API itself.
Endless Holes
I know that this is just the tip of the security iceberg. And I know that real "security types" could add sooooo much more to such an article. But I wanted to point out a few of these horrific (and silly) flaws because it seems that I've been running into them with ever-greater frequency in the last year-or-two.
IMHO, there's a common theme that API developers tend to view the API as just an extension of the application's internal logic.
They wouldn't create a form field in the app's UI that allowed the user to manually set the value for saveRate
, irrespective of the values for saves
and shotsOnGoal
. But they'll create a REST endpoint that allows you to PUT
a manual value for saveRate
- even if it doesn't mesh with the other values.
They wouldn't expose their database schema on the frontend. But they'll create a REST endpoint that basically represents a full schema diagram - complete with credentials that match those of the API user.
Too often, we design APIs to be inherently trusting of the API user - because the API user is a "technical" user. In other words, the API user is more like "us" - the programmers - than like the end users. But this scenario shouldn't make us more trusting. It should make us more wary.
Top comments (9)
You are right that lots of web security protocols just use a "God Rights" key that allows any REST call to do anything. The more appropriate way is to have any "caller" of the remote service be required to have an RSA PKE key such that every call is done with some 'identity', and to each of these identities the system knows which privileges are allowed (which buildings are controllable, what things are able to be deleted, etc).
Of course going along with this architecture you can have the requester be required to do a PKE based digital signature of the request (to send along with the request, like in an HTTP header) so that the server side can also be certain the request did indeed originate from who it's supposed to have.
Having HTTPS makes some of this redundant, so the real key thing I'm saying here is:
1) Don't use tokens with God Rights
2) Make each user/role have the minimal privileges that it's required to have so that there's no way it can go rogue even if hacked.
Totally agree.
Yes! Too many developers see web APIs as another interface between areas of the code little different to a function call or an object.
A Web API doesn't support the front-end, it is the front-end (at least when it comes to security and logical control).
We must assume that if it can be done, at some point it will be done... if something is behaviorally important, the mechanisms of the API should actually enforce the behavior, not just allow the behavior.
Exactly!
There are times I will say there is a security flaw in an endpoint, and my business partner will say "but chances of that are low, I'm not worried. Push it.".
SMH. So true, but still... SMH.
Loving this series
Hi Adam,
Great article. I was hoping if you could demonstrate more by adding technical examples.
It's more of a concept than a technical example. But to try to be a little more specific, I've recently been working on a system that manages compliance on individual machines. "Compliance" is a concept that is derived from many different factors. If the device doesn't have updated antivirus files, or if it's outside its acceptable geographic area, or if it's not encrypted, or if many other factors - it's noncompliant.
But when you look at the API for the software, there's a
PUT /v1/device/{id}
endpoint that allows you to update - directly - the "compliant"true/false
value. But that makes no sense. You shouldn't be able to just set a device to be "compliant". It should be calculated to be compliant (or non-compliant) based on all the other factors.