OWASP API - Top Ten
OWASP Top 10 API Security Risks
Source: https://owasp.org
According to OWASP, the following are the top 10 API security risks:
API1: Broken Object-Level Authorization
- APIs often reveal endpoints that manage object identifiers, thereby broadening the attack surface with object-level access control vulnerabilities. It is essential to implement object-level authorization checks in every function that retrieves data using an ID provided by the user.
- Attackers can exploit API endpoints that are vulnerable to broken object-level authorization by manipulating the ID of an object that is sent within the request.
- Unauthorized access to other users' objects can result in data disclosure to unauthorized parties, data loss, or data manipulation.
The Problem: You're not checking if users can access specific objects/resources.
Bad Code Example:
// BAD - No authorization check
app.get("/api/users/:userId/profile", (req, res) => {
const profile = getUserProfile(req.params.userId);
res.json(profile);
});
Good Code Example:
// GOOD - Check if authenticated user can access this profile
app.get("/api/users/:userId/profile", authenticateUser, (req, res) => {
const requestedUserId = req.params.userId;
const currentUserId = req.user.id;
// Only allow users to access their own profile
if (requestedUserId !== currentUserId && !req.user.isAdmin) {
return res.status(403).json({ error: "Forbidden" });
}
const profile = getUserProfile(requestedUserId);
res.json(profile);
});
API2: Broken Authentication
- Authentication mechanisms are often implemented incorrectly, allowing attackers to compromise authentication tokens or to exploit implementation flaws to assume other user's identities.
- Attackers can gain complete control of other users' accounts in the system, read their personal data, and perform sensitive actions on their behalf.
The Problem: Weak authentication implementation allows token theft or identity spoofing.
Common Mistakes:
- Storing tokens in localStorage (vulnerable to XSS)
- Weak JWT secrets
- No token expiration
- Accepting tokens in URL parameters
Better Approach:
// Use httpOnly cookies for tokens
app.post("/login", (req, res) => {
// Validate credentials...
const token = jwt.sign({ userId }, process.env.JWT_SECRET, {
expiresIn: "15m",
});
res.cookie("token", token, {
httpOnly: true,
secure: true, // HTTPS only
sameSite: "strict",
maxAge: 15 * 60 * 1000, // 15 minutes
});
});
API3: Broken Object Property Level Authorization
- While designing the API, the developers may expose all the object properties to the clients without considering their individual sensitivity and depend on the clients for filtering data.
- Unauthorized access to private/sensitive object properties may result in data disclosure, data loss, or data corruption.
- Under certain circumstances, unauthorized access to object properties can lead to privilege escalation or partial/full account takeover.
The Problem: Exposing sensitive fields that users shouldn't see.
Bad Code:
// BAD - Exposing everything
app.get("/api/users/:id", (req, res) => {
const user = database.users.findById(req.params.id);
res.json(user); // Includes password, ssn, internal flags, etc.
});
Good Code:
// GOOD - Filter sensitive data based on user role
app.get("/api/users/:id", (req, res) => {
const user = database.users.findById(req.params.id);
const publicFields = {
id: user.id,
name: user.name,
email: user.email,
};
// Only admins see sensitive data
if (req.user.role === "admin") {
publicFields.ssn = user.ssn;
publicFields.internalNotes = user.internalNotes;
}
res.json(publicFields);
});
API4: Unrestricted Resource Consumption
- Attackers can initiate multiple concurrent requests using automated tools designed to cause DOS via high loads of traffic, impacting APIs' service rate.
- Attackers find APIs that do not limit client interactions or resource consumption and craft API requests including parameters that control the number of resources.
The Problem: No rate limiting allows DoS attacks.
Solution - Implement Rate Limiting:
const rateLimit = require("express-rate-limit");
// Basic rate limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: "Too many requests from this IP",
});
// Expensive operation needs stricter limits
const searchLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 5, // only 5 searches per minute
});
app.use("/api/", limiter);
app.use("/api/search", searchLimiter);
API5: Broken Function-Level Authorization
- Complexity in access control policies through different hierarchies, groups, and roles between administrative and regular functions can cause authorization errors.
- Allow attackers to gain unauthorized access to administrative functions or users' resources.
The Problem: Users accessing admin functions they shouldn't.
Bad Code:
// BAD - Only checking authentication, not authorization
app.delete("/api/users/:id", authenticateUser, (req, res) => {
deleteUser(req.params.id);
res.json({ success: true });
});
Good Code:
// GOOD - Check specific permissions
const requireRole = (role) => (req, res, next) => {
if (req.user.role !== role) {
return res.status(403).json({ error: "Insufficient permissions" });
}
next();
};
app.delete(
"/api/users/:id",
authenticateUser,
requireRole("admin"),
(req, res) => {
deleteUser(req.params.id);
res.json({ success: true });
},
);
API6: Unrestricted Access to Sensitive Business Flows
- APIs vulnerable to this risk expose a business flow such as buying a ticket, or posting a comment without compensating for how the functionality could harm the business if used excessively in an automated manner.
- Attackers manually identify which resources are involved in the target workflow and how they work together.
- Attackers understand the business model backed by the API, finding sensitive business flows and automating access to these flows, causing harm to the business.
The Problem: No protection against automated abuse of business processes.
Example Solution:
// Protect ticket purchasing from bots
const ticketPurchaseLimiter = rateLimit({
windowMs: 5 * 60 * 1000, // 5 minutes
max: 2, // max 2 ticket purchases per 5 minutes
keyGenerator: (req) => req.user.id, // per user, not per IP
});
app.post(
"/api/tickets/purchase",
authenticateUser,
ticketPurchaseLimiter,
validateCaptcha, // additional bot protection
(req, res) => {
// Purchase logic here
},
);
API7: Server-Side Request Forgery
- A server-side request forgery (SSRF) flaw enables an attacker to coerce the application to send a crafted request to an unexpected destination, even when protected by a Firewall or a VPN.
- Attackers exploit this vulnerability by finding an API endpoint that accesses a URI provided by the client.
- Exploitation might lead to internal services enumeration (e.g., port scanning), information disclosure, bypassing firewalls, or other security mechanisms.
The Problem: API makes requests to URLs provided by users.
Bad Code:
// BAD - Directly using user-provided URL
app.post("/api/fetch-image", (req, res) => {
const imageUrl = req.body.url;
fetch(imageUrl).then((response) => {
// Process image...
});
});
Good Code:
// GOOD - Validate and restrict URLs
app.post("/api/fetch-image", (req, res) => {
const imageUrl = req.body.url;
// Validate URL format
if (!isValidHttpUrl(imageUrl)) {
return res.status(400).json({ error: "Invalid URL" });
}
// Block internal/private networks
const url = new URL(imageUrl);
if (isPrivateIP(url.hostname) || url.hostname === "localhost") {
return res
.status(400)
.json({ error: "Access to private networks forbidden" });
}
// Only allow specific domains
const allowedDomains = ["cdn.example.com", "images.trusted-site.com"];
if (!allowedDomains.includes(url.hostname)) {
return res.status(400).json({ error: "Domain not allowed" });
}
fetch(imageUrl).then((response) => {
// Process image...
});
});
API8: Security Misconfiguration
- Attackers will often attempt to find unpatched flaws, common endpoints, services running with insecure default configurations, or unprotected files and directories to gain unauthorized access or knowledge of the system.
- Attackers use automated tools to detect and exploit misconfigurations such as unnecessary services or legacy options.
- Security misconfigurations not only expose sensitive user data but also system details that can lead to a full server compromise.
The Problem: Default configs, unnecessary services, verbose errors.
Key Dev Practices:
// Hide framework details
app.disable("x-powered-by");
// Proper error handling - don't leak stack traces
app.use((err, req, res, next) => {
console.error(err.stack); // Log for debugging
// Don't send stack traces to clients
res.status(500).json({
error:
process.env.NODE_ENV === "production"
? "Internal server error"
: err.message,
});
});
// Security headers
app.use((req, res, next) => {
res.setHeader("X-Content-Type-Options", "nosniff");
res.setHeader("X-Frame-Options", "DENY");
res.setHeader("X-XSS-Protection", "1; mode=block");
next();
});
API9: Improper Inventory Management
- Proper inventory management of hosts and deployed API versions is important to mitigate issues.
- Attackers usually gain unauthorized access through old API versions or endpoints left running unpatched and using weaker security requirements.
- Attackers can gain access to sensitive data or even take over the server when different API versions/deployments are connected to the same database with real data.
The Problem: Old API versions still running with weaker security.
Dev Best Practices:
- Version your APIs (
/api/v1/,/api/v2/) - Have a deprecation strategy
- Monitor and log API version usage
- Implement sunset headers for old versions
// Version-specific middleware
app.use("/api/v1", (req, res, next) => {
res.setHeader("Sunset", "Sat, 31 Dec 2023 23:59:59 GMT");
res.setHeader("Deprecation", "true");
next();
});
API10: Unsafe Consumption of APIs
- Developers tend to trust data received from third-party APIs more than user input and thus tend to adopt weaker security standards.
- Attackers go after integrated third-party services instead of trying to compromise the target API directly.
- Successful exploitation may lead to exposure of sensitive information to unauthorized actors, different types of injections, or denial of service.
The Problem: Trusting third-party API data too much.
Bad Practice:
// BAD - Trusting third-party data
app.get("/api/weather", async (req, res) => {
const weatherData = await fetch("https://weather-api.com/current");
const data = await weatherData.json();
// Directly using unvalidated data
res.json(data);
});
Good Practice:
// GOOD - Validate and sanitize third-party data
app.get("/api/weather", async (req, res) => {
try {
const weatherData = await fetch("https://weather-api.com/current");
const rawData = await weatherData.json();
// Validate the structure and content
const validatedData = {
temperature: typeof rawData.temp === "number" ? rawData.temp : null,
condition:
typeof rawData.condition === "string"
? sanitizeString(rawData.condition)
: "Unknown",
// Only include fields you expect and validate
};
res.json(validatedData);
} catch (error) {
res.status(503).json({ error: "Weather service unavailable" });
}
});
Quick Developer Checklist:
- ✅ Always check user permissions for specific resources
- ✅ Use secure authentication patterns (httpOnly cookies, strong secrets)
- ✅ Filter response data based on user roles
- ✅ Implement rate limiting on all endpoints
- ✅ Validate user roles before allowing actions
- ✅ Add business logic protection (captcha, limits)
- ✅ Never trust user-provided URLs
- ✅ Remove default configs and verbose errors
- ✅ Maintain API version inventory
- ✅ Validate all third-party API data