API

API Security: Best Practices for Python Developers - Part I

Developer’s Guide for a secure API implementation. Devs are the core of web applications, however, they are also the ones who end up introducing and deploying vulnerabilities that later get exploited. That's why you should know how to prevent common attacks and secure your endpoints correctly.

· 7 min read
API Security: Best Practices for Python Developers - Part I

We are here to show you the OWASP Top 10 API Security to develop secured coding and stand out from other developers. Know your vulnerabilities!

First, what's OWASP Top 10?

The Open Web Application Security Project (OWASP) is an open community dedicated to improving the security of software through its community-led open-source software projects.

The OWASP Top 10 is a standard awareness document for developers and web/mobile application security. It represents a broad consensus about the most critical security risks to applications. That's why this is the start of your new security journey, and we are here to guide you along the way.

Now, let's start enumerating Best Security Practices!

1. Validate who is requesting the resource.

Vulnerability: Broken Object Level Authorization
Attackers can exploit API endpoints by manipulating the ID of an object that is sent within the request. This may lead to unauthorized access to sensitive data because the server usually doesn't fully track the client’s state, and instead, relies more on parameters like object IDs, that are sent from the client to decide which objects to access.

# Vulnerable endpoint
@app.route("/user/<objectid>", methods=["GET"])
def get_document(objectid):
    if exists(objectid):
      # Just fetches object without checking the ownership
      obj = retrieve(objectid)
      return obj, 200
    else:
      msg = "Document doesn't exist"
      return msg, 404

This mainly occurs because the server is not checking whether the user requesting the object should be able to see it. And if the object IDs are sequential (e.g. 1000, 1001, 1002, and so on), then it's easy to guess the next object ID to request. Even though it's not yours to see.

How to fix this vulnerability

# Secure endpoint
@app.route("/user/<objectid>", methods=["GET"])
def get_document(objectid):
    if exists(objectid):
      user = get_user_information()

      # Validation - Checks for permissions
      if has_access(user, objectid):
        obj = retrieve(objectid)
        return obj, 200
      else:
        msg = "You don't have access to this document"
        return msg, 403
        
    else:
      msg = "Document doesn't exist"
      return msg, 404

Other tips for fixing and preventing Broken Object Level Authorization vulnerabilities:

  • Prefer to use random and unpredictable values as GUIDs for records’ IDs. Implement libraries for this, like uuid
  • Implement a proper authorization mechanism that relies on the user policies and hierarchy, like Django Groups
  • Use an authorization mechanism to check if the logged-in user has access to perform the requested action on the record in every function that uses input from the client to access a record in the database.

2. Implement Rate Limiting.

Vulnerability: Broken Authentication
Authentication in APIs is a complex mechanism. Besides, it is an easy target for attackers, since it’s exposed to everyone. These two points make the authentication component potentially vulnerable to many exploits and attacks.

A proper authentication mechanism should be a combination of the following:
- Something you know -> Like username and password.
- Something you have -> Like a security key or code sent to cellphone.
- Something you are -> Like your fingerprint or bio-metric data.

When implementing any of these authentication mechanisms, make sure to limit the amount of attempts a user can make, because if that isn't the case, with enough time this process can be automated to brute-force the endpoint with different passwords / security codes until one is correct.

# Vulnerable endpoint
@app.route("/verifyOTP", methods=["POST"])
def verifyOTP():
    received_code = request.data.code
    correct_code = get_generated_code()
    
    if received_code == correct_code:
      log_user_in()
      msg = "Logged in successfully"
      return msg, 200
    else:
      msg = "Wrong verification code"
      return msg, 403
💡
PRO TIP: Forgot Password and Reset Password endpoints should be treated the same as authentication mechanisms. As an attacker can bypass authentication by exploiting this mechanisms without knowing users' credential

How to fix this vulnerability

Normally, Rate Limiting for Logins take around 3 to 5 wrong attempts to lock the account. Let the user know the amount of wrong attempts it has before locking the account for a predetermined period of time.

# Secure endpoint
@app.route("/verifyOTP", methods=["POST"])
def verifyOTP():
    # Validation - Checks how many times user attempted the verifyOTP
    if has_available_attempts():
      received_code = request.data.code
      correct_code = get_generated_code()
      
      if received_code == correct_code:
        log_user_in()
        msg = "Logged in successfully"
        return msg, 200
      else:
        msg = "Wrong verification code. You only have 5 attempts"
        has_available_attempts(-1)
        return msg, 403

    else:
      msg = "Too many wrong requests. Try again in an hour"
      return msg, 429

Other tips for fixing and preventing Broken Authentication vulnerabilities:

  • Don't reinvent the wheel in authentication, token generation, and password storage. Use the standards and libraries available.
  • Credential recovery / forgot password endpoints should be treated as login endpoints in terms of brute force, rate limiting and lockout protections.
  • Where possible, implement multi-factor authentication.
  • Implement anti brute-force mechanisms to mitigate credential stuffing, dictionary attack and brute force attacks on your authentication endpoints. Consider rate limits by API method (e.g. authentication), client (e.g. IP address), property (e.g. username).
  • Implement account lockout / captcha mechanism to prevent brute force against specific users.
  • Implement weak-password checks.
  • API keys should not be used for user authentication, but for client app / project authentication.

3. Don't share more information than it's needed.

Vulnerability: Excessive Data Exposure
Exploitation of Excessive Data Exposure is simple, and is usually performed by sniffing the traffic to analyze the API responses, looking for sensitive data exposed that shouldn't be returned to the user.

APIs usually rely on the front-end to perform the data filtering because they are normally used as data sources. Sometimes developers try to implement API endpoints in a generic way without thinking about the sensitivity of the exposed data so that they can be reused in different parts of the application.

This is easy for a normal user to exploit, as the whole API response can be seen from the Browser's Web Developer Tools. And we all have one of those on our computers.

How to fix this vulnerability

The front-end is not the one to perform the filtering of data, as it can be easily bypassed. That kind of validation and filtering should be done by the server, and only the necessary information should be sent on each request.

Other tips for fixing and preventing Excessive Data Exposure vulnerabilities:

  • Never rely on the client side to perform sensitive data filtering. Send filtered information directly from the backend.
  • Review the responses from the API to make sure they contain only the necessary data.
  • Define all sensitive and personally identifiable information (PII) that your application stores and make sure they are stored correctly and secured.

4. Protect your server's resources.

Vulnerability: Lack of Resources & Rate Limiting
This exploitation is simple and no authentication is required on most cases. Multiple concurrent requests can be performed from a single local computer to consume all server's resources. It’s common to find APIs that do not implement rate limiting or where size validations aren't properly set. Exploitation may lead to DoS, making the API unresponsive or even unavailable to legitimate users.

# Vulnerable endpoint
@app.route("/image", methods=["POST"])
def upload_image():
    image = request.data.image
    
    if save(image) == True:
      msg = "Image uploaded successfully"
      return msg, 200
    else:
      msg = "An error occurred. Try again later"
      return msg, 500

If someone is feeling naughty that day, the image uploading steps could be automated and be left running so heavy images get uploaded to the server several times. Consuming all server resources over time.

How to fix this vulnerability

Rate limiting should be implemented to give only a few uploads to each user for a set period of time to prevent this type of automated attacks. Also, it's always a great idea to validate the size of the resources that get uploaded to or requested from the application. So the server doesn't get stuck processing big chunks of data.

# Secure endpoint
@app.route("/image", methods=["POST"])
def upload_image():
    image = request.data.image

    if can_upload():

      if too_big(image.size):
        msg = "Image size is too big. Choose another file"
        return msg, 400
    
      if save(image) == True:
        msg = "Image uploaded successfully"
        return msg, 200
      else:
        msg = "An error occurred. Try again later"
        return msg, 500

    else:
      msg = "You already uploaded too many images. Erase one to continue."
      return msg, 400

Other tips for fixing and preventing Lack of Resources & Rate Limiting vulnerabilities:

  • Implement a limit on how often a client can call the API within a defined time frame.
  • Notify the client when the limit is exceeded by providing the limit number and the time at which the limit will be reset.
  • Add proper server-side validation for query string and request body parameters, specifically the ones that can be manipulated by users.
  • Define and enforce maximum size of data on all incoming parameters and payloads such as maximum length for strings and maximum number of elements in arrays.
  • If your API accepts compressed files check compression ratios before expanding the files to protect yourself against "zip bombs".

5. Check who is performing the action every single time:

Vulnerability: Broken Function Level Authorization
Exploitation requires the attacker to send legitimate API calls to endpoints that they shouldn't have access to. However, if these APIs don't properly check the user's permissions, everyone would be able to trigger sensitive functionality.

# Vulnerable endpoint
@app.route("/user/<userid>", methods=["DELETE"])
def delete_user(userid):
    if exists(userid):
      # Perfroms unsafe action
      delete(userid)
      msg = "User deleted successfully"
      return msg, 200
    else:
      msg = "User  doesn't exist"
      return msg, 404

How to fix this vulnerability

# Secure endpoint
@app.route("/user/<userid>", methods=["DELETE"])
def delete_user(userid):
    if exists(userid):
      logged_user = get_user_information()

      # Validation - Only an admin can delete a user
      if is_admin(logged_user):
        delete(userid)
        msg = "User deleted successfully"
        return msg, 200
      else:
        msg = "You don't have access to perform this action"
        return msg, 403
        
    else:
      msg = "User doesn't exist"
      return msg, 404

Other tips for fixing and preventing Broken Function Level Authorization vulnerabilities:

  • Don't rely on the frontend of you application to show / hide buttons or calls with sensitive actions depending on the user. You should block backend endpoints instead by validating user permissions.
  • The enforcement mechanism(s) should deny all access by default, as known as blacklists / denylists, requiring explicit grants to specific roles for access to every function.

Time to take action

If you ended up here with us is because you're interested in improving your developer skills by implementing Security Best Practices. We guided you here, now it's your turn to assess your API and patch any vulnerabilities you may have.

Did you like what you read? Because there is a PART TWO waiting for you soon in our blog. In the meantime, check the other posts out here. See you over there!

________________________________________________________________________


Check our other social media platforms to stay connected:‎

Website | www.vidocsecurity.com
Linkedin | www.linkedin.com/company/vidoc-security-lab
X (formerly Twitter) | twitter.com/vidocsecurity
YouTube | www.youtube.com/@vidocsecuritylab
Facebook | www.facebook.com/vidocsec
Instagram | www.instagram.com/vidocsecurity