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.
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.
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:
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).
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.
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
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.