DEV Community

Theodoros Danos
Theodoros Danos

Posted on

Common Developer Pitfalls: A Guide to Bolstering Security

“Breaking their Defense” Series

In this series dive into the thrilling depths of my role as a penetration tester. Experience the adrenaline-pumping reality of simulating an attacker in real-life engagements, seen through my lens. Discover how I infiltrate what my clients proudly dub their 'digital fortress,' daringly challenging their perception of invincibility. My mission? To uncover not just that I can penetrate their defenses, but to identify and expose every chink in their armor that puts their most valuable asset at risk - information.

Previous Article: “Breaking their Defense - Hacking Stories”


Common Security Bugs

Let's dive into the common missteps developers often make - the pitfalls we frequently uncover during our penetration tests. By reading this insightful article filled with mini-stories about various bugs, you'll be able to zero-out the most common errors and bolster your security measures.

One to rule them all - Improper Access Controls

In our tests, the bug we run into the most is a missing 'Access Control' mechanisms. We often see that web or mobile apps skip crucial checks, letting users (those who aren't supposed to) see information they're not meant to. This usually happens as the folks building the back end of the app believe that if a user can't reach certain information through the normal user screen, they can't get to it at all. At other times, it's simply an oversight.

IDOR

The most common type of access control bug is what is called an IDOR (Insecure Direct Object Reference). What’s that? This is a particular type of bug were the request is based on an ID (the reference). Such ID represents the information to be fetched (the document, the database record, etc). Often, such IDs are predictable. For example, my documents shown through the User-Interface are “invoice-01.doc” and “invoice-02.doc”. Such documents to be retrieved can be fetched through the endpoint /documents/. The UI retrieves the previously mentioned documents by calling /documents/4145 and /documents/4159 respectively. However, based on the incremental nature of the ID, one could brute-force the range of documents (i.e from 0 to 10000) and retrieve all documents of all users. A proper access control shouldn’t allow that. Don’t be fooled by the assumption that the type of interface you are using is safer as that doesn’t make a difference - GraphQL, RESTful or any other kind of interface.

Modern apps often use UUIDs, which are unique 128-bit IDs, rather than simple IDs. For instance, a UUID might look like this: '47b3c2cc-3f5c-46d5-a84a-22ae5d3031a4'. It's pretty tough to guess these UUIDs and randomly find a document. But, if someone happens to see the UUID (maybe they peek over your shoulder), they could use it to pull up the document in their own session. So, even though it might seem safer than using simple IDs, it can still be a security issue.

Unusual Ones

Some access controls can be quite unusual. Let me tell you a story. Once, I was working with an app that redirected you to the regular user interface if you tried to go to the admin area. It looked like a good security measure. But was it really?

What they did was load all the necessary JavaScript libraries for the admin UI without a problem. But before the page started loading any data, it would check with the server if the user was an admin. If yes, it would proceed to load the admin data onto the UI. If no, it would redirect the user back to their regular dashboard.

But there was a way around this. If we “skipped” the admin check and triggered the next steps manually, we could use the full admin UI! How did we do it? We cheated. We didn’t even had to skip it. We just changed the response from the server that the JavaScript saw. We made it look like the server said we were admins. After all, we control what happens on our side of the screen. That's the nature of client-side code. The issue wasn't with the front-end, it was with the back-end. They relied too heavily on the front-end to handle access control. Remember, access control should ALWAYS be implemented at the back-end.

Escalation of Privileges

Access control issues can sometimes lead to a problem known as privilege escalation. This is when a user gains more access rights or powers within a system than they should have. Although it's a big enough issue to talk about on its own, it often comes about because of other missing access controls. So, we're discussing it in this context.

Let's look at an example using the FastAPI Web Framework. This is a piece of software that changes a user's role within a system:

@app.post('/change_role')
async def change_role(role: UserRole, current_user: User = Depends(get_current_user)):

    user = User.query.get(role.user_id)
    if not user:
        raise HTTPException(status_code=400, detail='User does not exist')

    if role.new_role not in ['admin', 'user', 'guest']:
        raise HTTPException(status_code=400, detail='Invalid role')

    user.role = role.new_role
    db.session.commit()

    return {'message': 'Role changed successfully'}
Enter fullscreen mode Exit fullscreen mode

Even if you don't know much about coding, you can see there's a problem. The software doesn't check if a request is coming from a normal user or an admin user. This means a normal user could potentially change their own role and make themselves an admin. Once they've done that, they could go to the admin panel (because the system now thinks they're a real admin), and do anything an admin can do. You have no idea how often we find these kind of vulnerabilities - more often than you may think.

This simple code could have prevent the worst and fix the vulnerable code mentioned before:

if current_user.role != 'admin':
        raise HTTPException(status_code=400, detail='Only admins can change roles')
Enter fullscreen mode Exit fullscreen mode

Now, how can you tackle such problems? Usually, when handling data, you pull it from a database by making requests. You could create a separate request to confirm that the user has the right permissions. Or, you could include proper checks in a single request to ensure the retrieved data is appropriate for the user.

Take this as an example:

Missing an access control mechanism:

app.get('/user/:id', async (req, res) => {
    const user = await User.findById(req.params.id);
    res.json(user);
});
Enter fullscreen mode Exit fullscreen mode

An access control mechanism is in-place:

app.get('/user/:id', async (req, res) => {
[...]
        if(user.role === 'admin' || user.id === req.thisUser.id) {
            const userData = await User.findById(req.params.id); 
            res.json(userData); // Return user data
        } else {
            return res.sendStatus(403);
        }
[...]
Enter fullscreen mode Exit fullscreen mode

Injections

Injections are the second type of common misconfiguration. Injections is a generic category, an umbrella of specific injection vulnerabilities, which among others are “SQL and NoSQL Injection”, “XSS Injection”, “Command Injection”, “Code Injection” and more. The most common injections are the SQL and NoSQL Injections as well as XSS Injections.

Cross-Site Scripting

XSS vulnerabilities are also among the common ones. Such vulnerability deserves a category on its own but I have placed the vulnerability class under “injections” as the most usual XSS Vulnerabilities were raised through data injected by the attacker.

XSS vulnerabilities, meaning Cross-Site Scripting, is when an attacker injects a piece of client-side code which will be somehow called on the other user’s browser. Then, the code can steal the cookies that sets the session or even execute actions on behalf of the user by making requests to the server through the victim’s browser. This type of attack mostly happens on web applications where the browser is involved.

Here is an example of a vulnerable PHP Code snippet that lists the user’s usernames:

<?php
[...]
$result = $db->query("SELECT * FROM users");

echo "<ul>";
while ($row = $result->fetch_assoc()) {
    echo "<li>" . $row['username'] . "</li>";
}

echo "</ul>";
[...]
?>
Enter fullscreen mode Exit fullscreen mode

Then the attacker could change their own username and place the following payload instead:

<script>users.deleteUser(30)</script>John
Enter fullscreen mode Exit fullscreen mode

If the listing shown in administrator’s page, it will delete the user having ID 30. If listed on a normal user’s screen nothing will happen. In either case, the username list will include the username too, hiding the malicious code that has been executed.

SQL and NoSQL Injections

SQL and NoSQL Injections happen when an attacker manages to mix their own input with a database query. This usually happens when the system takes user data, doesn't properly sanitize it, and then includes it in a database request. The user could potentially alter the database query in ways that weren't planned by the application developers. So, it's very important to always clean the data first.

Many people these days use Object-Relational Mapping (ORM), which is a safer way to query databases. But when developers have to make their own custom queries, they often use prepared statements to keep things secure.

However, even with these tools, the most important factor is educating developers about security. For instance, I once found a blind SQL Injection attack that let me pull all the data from a database. I immediately let the customer know about this critical problem. I worked closely with their team to help fix it, and they told me it was resolved. But when I checked again, the problem was still there. They insisted they had used prepared statements, a recommended secure practice. But how they had used them showed that they hadn't been properly trained in writing secure code.

Here is a classic payload example that may bypass your login function. The following can be put into the password field:

' or 1=1 --
Enter fullscreen mode Exit fullscreen mode

I won't go into detail explaining SQL Injections here, as it's a well-known example and you can easily find explanations online.

If you rush to try this on your own app and it doesn't work, don't be misled. SQL Injection is almost like a science. We usually test a wide range of attack strategies based on the information we have, such as the type of database, any firewall rules, and more. Remember, the attack strategy can be changed over and over, depending on how the app responds.

Want to keep safe from SQL Injections? You can use prepared statements or an Object-Relational Mapping (ORM) model. But that's not enough. You should really understand the tools you're using. Don't just copy and paste code that seems to work without fully grasping what it does. Consider taking courses that focus on how to code securely. This way, you won't just know what functions to use, but also how to use them properly. Even then, you can reach out to us to make sure the code pass the penetration tests!

Let me upload a file

Many web applications we test allow users to upload files. If the file upload feature isn't designed carefully, it can lead to big problems.

In many cases, uploaded files are stored on the same web server that hosts the application code (this is common with PHP or ASP). So, if the application code is in a folder named /controller/, the files might be stored under /files/. How these files are treated - whether they're run as code or offered for download - depends on their file type, and that's decided by the web server when a request is made.

An attack using this vulnerability typically happens in two stages. First, a user uploads a file. Then, the user (or someone else) downloads that file, which triggers the server to do something with it. Attackers target the upload process to trick the web server into treating the uploaded file as executable code rather than a file for download (for example, uploading a PHP file instead of an image).

Once a user runs code that an attacker uploaded, it's basically all over. The server's interpreter runs the attacker's code just like it would any other part of the application.

There's no one-size-fits-all solution to this, but a good start is to use the secure file upload features that most web frameworks offer. If your application runs on the web server's interpreter, you could also consider these defensive steps:

Firstly, always change the permissions of the upload path to make it non-executable. This helps prevent remote code execution, even if a vulnerability is found.

Secondly, move the upload directory to a location that isn't accessible to an attacker, like /uploads/ instead of /var/www/html/uploads. Even if an attacker finds a vulnerability (which they shouldn't), they can't run the file because they can't ask the web server to download it. To allow downloads in this setup, you'd need to manually read the file and send it to the user, including the correct HTTP headers.


You are not untouchable

Some folks believe they're untouchable, writing code that's as secure as Fort Knox. But guess what? Eventually they get hacked! The best way to truly gauge your coding skills is to get a penetration test done. Let us find the loopholes in your system before someone else does. Above all, stay informed about the latest tech threats and how to guard against them.

Contact us now to talk about penetration testing or whatever else might bothering you.

White Hat Hackers are here to help you, so take advantage of us!

You can find our our offered services at https://cybervelia.com/

Top comments (0)