In this article
May 13, 2025
May 13, 2025

Secure by design: How engineers should build and consume APIs

A practical guide to avoiding common pitfalls and implementing security best practices across both internal and third-party API integrations.

Whether you're building your own APIs or consuming third-party ones, it's easy to overlook key security considerations—until something goes wrong. From data leaks and injection attacks to credential exposure and denial-of-service risks, APIs can be a soft target if not designed and managed carefully.

This article walks through the most common security pitfalls on both sides of the API equation—what developers get wrong, why it matters, and how to fix it. You'll also find actionable best practices to help you build and integrate APIs with confidence.

Common security pitfalls in API design

Even well-intentioned API designs can introduce serious vulnerabilities if security isn’t baked in from the start. Below are some of the most common mistakes developers make when building APIs—and how to avoid them before they become liabilities.

1. Lack of authentication and authorization controls

One of the most common—and dangerous—mistakes in API security is not properly controlling who can access your API and what they’re allowed to do once they get in. This typically comes down to two related but distinct issues: authentication (verifying the user’s identity) and authorization (determining what that user is allowed to access).

Without strong controls in place, anyone could potentially access sensitive data or perform unauthorized actions.

Some common issues we see are:

  • Public endpoints left open with no login or key required.
  • Missing token validation, allowing users to send expired or fake credentials.
  • Over-permissive access, where all users can access all resources regardless of their role.
  • Hardcoded credentials that never expire or rotate.

To avoid this:

  • Use industry-standard authentication protocols, such as OAuth 2.0 or OpenID Connect. These frameworks are widely adopted and provide secure, scalable mechanisms for verifying users and applications.
  • Enforce token validation on every request. This includes checking expiration, signature integrity, and scope. For more see JWT validation: how-to and best libraries to use.
  • Implement authorization. Not every user or system needs access to every resource.
  • Separate public and private endpoints clearly, and lock down anything sensitive with proper controls.
  • Rotate and revoke credentials regularly, and use short-lived tokens where possible.

2. Injection attacks

Injection attacks happen when an API takes untrusted input and passes it directly into a command, query, or interpreter—without properly validating or escaping it. This allows attackers to "inject" malicious code that the backend system executes, often with serious consequences.

The most common types include SQL injection, NoSQL injection, command injection, and LDAP injection, depending on what the backend is using.

Many things can go wrong when you blindly trust input:

  • Data exfiltration: Attackers retrieve sensitive data they shouldn't have access to.
  • Data manipulation: Attackers can insert, update, or delete records.
  • Remote code execution: In more severe cases, attackers may run arbitrary system commands.

To avoid this:

  • Never trust user input. Always assume input can be malicious, whether it comes from an end user or another system.
  • Use parameterized queries or prepared statements for all database interactions. These ensure that input is treated as data, not executable code.
  • Apply strict input validation and sanitize values where necessary—especially for fields like query parameters, headers, or body content.
  • Escape output if you're including user input in logs or responses.
  • Use schema validation libraries to enforce expected types, formats, and value ranges for input.

Parameterized query example

Let’s say a user is searching for a product by name. Instead of directly inserting user input into your SQL string (which is vulnerable to injection), you bind the input as a parameter.

This is how you would use a parameterized query in Python with psycopg2 for PostgreSQL:

  
import psycopg2

conn = psycopg2.connect("dbname=shop user=admin password=secret")
cur = conn.cursor()

user_input = "anything' OR '1'='1"  # malicious input

# Safe query using parameterized input
cur.execute("SELECT * FROM products WHERE name = %s", (user_input,))

rows = cur.fetchall()
  

In this version, using the  %s placeholder, psycopg2 handles the escaping and safely binds the input as a value, not as part of the SQL command. Even if the input contains ' OR '1'='1, it will not break the query or return unintended results.

If you were to do this instead:

  
query = f"SELECT * FROM products WHERE name = '{user_input}'"
cur.execute(query)
  

You’d be manually building a query string with unsanitized input. That’s dangerous.

3. Overexposure of data

Sometimes APIs give away more than they should—either by design or accident. This is known as overexposure of data, and it's a common security flaw that can lead to data leaks, privacy violations, and even regulatory non-compliance.It often happens when APIs return entire objects or database rows without filtering or masking sensitive fields like passwords, internal IDs, or confidential business logic.Some common examples are:

  • Returning full user records, including fields like password_hash, ssn, or internal_notes.
  • Exposing unnecessary metadata, such as internal database IDs or system configurations.
  • Forgetting to filter fields in nested objects. For example, you might have an API endpoint that returns an order summary, and each order includes a nested customer object. Even if the order fields themselves are safe, you’ve now exposed PII (personally identifiable information) and possibly even regulated data like a Social Security Number.
  • Exposing to end users detailed debugging info (stack traces, file paths, or error logs) that might leak file names, database tables, infrastructure details, sensitive config values, or environment variables.

To avoid this:

  • Use field whitelisting, not blacklisting: Explicitly define which fields are allowed in responses. Don’t just hide sensitive ones—only include what’s truly needed.
  • Create response schemas or DTOs (Data Transfer Objects): In frameworks like FastAPI, Django, or Express, define response schemas that tightly control what gets returned. This adds a layer of protection and documentation.
  • Apply access-based filtering: Customize responses based on user roles or permissions. For example, admins might see internal fields; regular users shouldn’t.
  • Mask or omit PII (Personally Identifiable Information): Be especially careful when returning user data. Mask or remove sensitive info entirely unless there's a compelling, secure reason to include it.
  • Log and audit what you send out: Keep logs of what’s being returned by the API—especially for sensitive routes. This helps in post-incident reviews and security audits.

Overexposed API example

Imagine your API returns detailed information about a project, including nested objects like tasks, users, and internal audit logs.

	
{
  "project_id": "P12345",
  "name": "Q2 Roadmap",
  "status": "active",
  "created_by": {
    "user_id": "U789",
    "name": "Jane Doe",
    "email": "jane@example.com",
    "role": "admin",
    "password_hash": "$2b$10$...",
    "two_factor_enabled": true,
    "last_login_ip": "192.168.1.10",
    "internal_notes": "Was warned for compliance violation"
  },
  "tasks": [
    {
      "task_id": "T001",
      "title": "Design homepage layout",
      "assignee": {
        "user_id": "U456",
        "name": "Tom",
        "email": "tom@example.com"
      },
      "debug_info": {
        "sql": "SELECT * FROM tasks WHERE id = T001",
        "execution_time_ms": 130,
        "server_region": "us-west-1"
      }
    }
  ],
  "audit_logs": [
    {
      "event": "project_created",
      "timestamp": "2024-10-11T09:45:00Z",
      "performed_by": "U789",
      "ip_address": "172.31.0.12",
      "db_snapshot": {
        "raw_json_dump": "{...}"
      }
    }
  ]
}
	

There are a lot of problems with this response:

  • PII and sensitive user metadata (email, password_hash, last_login_ip, internal_notes)
  • Infrastructure leakage (debug_info, server_region, raw SQL queries)
  • Security-sensitive fields (2FA status, internal audit logs, IP addresses)
  • Verbose nested objects (like full db_snapshot in audit logs)

These could lead to user tracking or profiling by malicious actors, violation of privacy regulations like GDPR, insight into database structure or cloud architecture, and more.

This is how a cleaned-up production-safe response looks:

	
{
  "project_id": "P12345",
  "name": "Q2 Roadmap",
  "status": "active",
  "created_by": {
    "user_id": "U789",
    "name": "Jane Doe"
  },
  "tasks": [
    {
      "task_id": "T001",
      "title": "Design homepage layout",
      "assignee": {
        "user_id": "U456",
        "name": "Tom"
      }
    }
  ]
}
	

4. Improper rate limiting

APIs without proper rate limiting are like open doors—eventually, someone will try to barge through. Whether it's a malicious actor launching a denial-of-service (DoS) attack or an accidental flood from a misconfigured client, unbounded request traffic can quickly overload your systems or expose vulnerabilities.

Rate limiting isn’t just about protecting infrastructure—it’s a key part of your API's security and reliability posture.

To avoid this:

  • Set per-user and per-IP rate limits: Implement limits like 100 requests per minute per IP or 1,000 per hour per user/token. Tailor thresholds based on user roles or subscription tiers (e.g., free vs. premium).
  • Use an API gateway or reverse proxy: Tools like Kong, AWS API Gateway, Cloudflare, NGINX, or Envoy make it easy to set and enforce rate limits at the edge. These solutions can also block suspicious patterns before they reach your app.
  • Include rate limit headers in responses: Let clients know how close they are to hitting the limit. The standard headers are X-RateLimit-Limit: 100, X-RateLimit-Remaining: 42, X-RateLimit-Reset: 1691605200
  • Use throttling and exponential backoff: Add short delays to slow down repeated or failed attempts—especially on sensitive endpoints like login or password reset. For example: 1s → 2s → 4s delays between failed login attempts.
  • Monitor and alert on anomalies: Watch for spikes in requests by user, IP, or region. Combine with logging and alerting systems (e.g., Grafana, Prometheus, Sentry) to catch abuse early.

5. Lack of logging and monitoring

APIs without proper logging and monitoring are flying blind. When something goes wrong—like a security breach, performance spike, or abuse—you need visibility to investigate, respond, and recover.

Unfortunately, many APIs log too little, log the wrong things, or don’t monitor at all until it’s too late.

Some common issues we see are:

  • No logs for failed login attempts or suspicious access patterns.
  • No monitoring for traffic spikes or repeated errors.
  • Logging sensitive data (like passwords or tokens) in plaintext.

To avoid this:

  • Log key events: authentication attempts, permission denials, data changes, and errors.
  • Use structured, centralized logging tools (e.g., ELK, Datadog, or CloudWatch) to enable filtering and alerting.
  • Mask or redact sensitive fields in logs—never store passwords, tokens, or personal identifiers.
  • Set up alerts for unusual behavior (e.g., too many 401s, IP-based request floods).
  • Correlate logs with metrics and traces for full visibility.

Best practices when building secure APIs

  • Implement TLS everywhere: Enforce HTTPS and disable HTTP entirely.
  • Enable CORS safely: Use CORS headers judiciously and whitelist trusted origins only. Avoid wildcards (*) unless absolutely necessary—and never use them with Access-Control-Allow-Credentials.
  • Validate input and output: Use schema validation libraries (e.g., JSON Schema, Joi) to strictly define what requests/responses should contain.
  • Enforce Content-Type headers strictly: Reject requests with incorrect or missing Content-Type headers (e.g., expect application/json, not text/plain).
  • Use API Keys and access tokens securely
    • Allow for revocation and rotation of API keys, OAuth tokens, and refresh tokens.
    • Implement expiration policies to reduce the blast radius of leaked credentials.
    • Provide mechanisms for users or admins to revoke compromised credentials instantly.
    • Store credentials in secure vaults.
  • Adopt the principle of least privilege: Limit what each endpoint, user, or token can do.
  • Keep dependencies updated: Stay current with libraries, frameworks, and SDKs to patch known vulnerabilities.
  • Version your APIs
  • : Avoid breaking changes and allow clients to continue using older, stable versions.
  • Regularly test and audit your API:
    • Use automated security scanning (e.g., OWASP ZAP, Snyk, Burp Suite).
    • Schedule penetration tests—especially before launches or major changes.
    • Run dependency vulnerability checks as part of your CI/CD pipeline.
  • Minimize trust between services: Even internal microservices should authenticate and use mutual TLS where appropriate. Avoid assuming that internal traffic is "safe"—internal breaches or misconfigurations happen.
  • Use security headers in responses: Add HTTP headers that enforce browser-based protections:
    • Content-Security-Policy
    • Strict-Transport-Security
    • X-Content-Type-Options
    • X-Frame-Options

Common pitfalls when consuming third-party APIs

Third-party APIs can be powerful tools—but they’re also potential liabilities. When you integrate external services, you extend your trust boundary and inherit the security posture of another system. Failing to vet and safely integrate these APIs can open the door to data leaks, system compromise, and operational instability.

These are the most common missteps to avoid.

1. Blind trust in providers

It’s easy to assume that well-known API providers have airtight security—but that’s not always the case. Even reputable services have suffered breaches, outages, or misconfigurations that impacted downstream consumers. Without due diligence, you could be depending on a system that’s vulnerable, poorly maintained, or unresponsive to incidents.

To avoid this:

  • Review the provider’s security documentation, terms of service, and incident history.
  • Check for compliance certifications like SOC 2, ISO 27001, or GDPR alignment.
  • Evaluate SLAs and uptime guarantees before relying on the service for critical features.

2. Insecure data transfers

Communicating with APIs over insecure channels (like HTTP) or failing to validate TLS certificates exposes your requests to interception, tampering, or replay attacks. This is especially risky when sending sensitive data like tokens, user IDs, or payment information.

To avoid this:

  • Enforce HTTPS-only connections—disable fallback to HTTP at all levels.
  • Validate TLS certificates to prevent MitM attacks and avoid accepting self-signed certs in production.
  • Implement certificate pinning where feasible, especially in mobile or high-assurance applications.

3. Leaking API keys and secrets

It’s alarmingly common for developers to accidentally commit API keys to public repositories or include them in frontend code. Once exposed, those keys can be harvested by attackers and used to make unauthorized requests, drain quotas, or access sensitive data.

To avoid this:

  • Keep secrets out of version control by using .env files and secure vaults like AWS Secrets Manager or HashiCorp Vault.
  • Interact with third-party APIs server-side only whenever possible, using your backend to proxy authenticated requests.
  • Enable key rotation and monitor usage logs to detect misuse.
  • Use scoped API keys with limited permissions rather than full-access credentials.

4. Trusting unvalidated responses

External APIs can fail or behave unpredictably—returning malformed, inconsistent, or even malicious data. If your system blindly accepts and processes these responses, you risk data corruption, logic errors, or unintended security flaws.

To avoid this:

  • Validate all API responses against a strict schema or type system (e.g., using Zod, Joi, or JSON Schema).
  • Treat all third-party data as untrusted—even if it’s coming from a “secure” source.
  • Implement defensive coding to handle missing, unexpected, or duplicated values gracefully.
  • Sanitize any third-party content before storing it, displaying it to users, or passing it downstream.

Best practices when consuming third-party APIs

  • Vet the API provider thoroughly: Before integrating any external API—whether free or paid—evaluate the provider like you would a critical dependency. A flaky or insecure API can cause downstream failures or expose your app to risk.
  • Validate and sanitize external responses: Even the most trusted APIs can return unexpected results—either due to version drift, outages, or undocumented changes. Never assume third-party responses are safe or well-formed.
  • Use scopes and permissions wisely: When using OAuth tokens, request only the scopes your application needs.
  • Always validate tokens: Enforce token validation on every request. This includes checking expiration, signature integrity, and scope.
  • Implement timeouts and retries: Prevent cascading failures and long hangs by setting sensible timeout values and retry logic. Plan for failures as a normal part of integration—not an exception.
    • Add retry logic with exponential backoff for recoverable errors (like 429 or 503).
    • Provide user-friendly fallback behavior when an API is unreachable (e.g., cached data or disabled features).
    • Log all failures with enough context to support quick debugging or escalation.
  • Sandbox and test integrations: Use staging environments or sandbox APIs to evaluate third-party behavior before production integration.
  • Monitor for deprecations: Stay alert to API version changes or deprecations announced by providers.
  • Monitor usage and performance continuously: Without observability, you won’t know when something breaks—or when a third-party API starts misbehaving.
    • Track latency, error rates, and volume of external API calls.
    • Set up alerts for unexpected traffic spikes, failures, or high response times.
    • Review usage logs periodically to ensure the integration is operating as expected.
  • Rate limit your requests: Be a good consumer—respect the provider’s rate limits and implement backoff logic.
  • Encrypt and store responses securely: Especially if the data includes personal or sensitive information, encrypt it at rest and in transit.

Conclusion

API security isn’t just about avoiding major failures—it’s about designing with intention, reducing risk, and building trust into every layer of your system.

As your API surface grows, the potential attack vectors grow with it. But with a clear understanding of the most common pitfalls—and a commitment to best practices—you can prevent most threats before they ever reach production.

Whether you’re publishing APIs or consuming them, secure-by-default principles, careful validation, access control, and observability go a long way. Build like every request could be an attack—and you’ll be ready when one is.

This site uses cookies to improve your experience. Please accept the use of cookies on this site. You can review our cookie policy here and our privacy policy here. If you choose to refuse, functionality of this site will be limited.