Welcome to the very first lesson of the "Security Misconfiguration" course! In this lesson, we'll explore the concept of default credentials and their impact on web application security. Default credentials are pre-set usernames and passwords that come with many applications and devices. While convenient for initial setup, they pose significant security risks if not changed.
By the end of this lesson, you'll learn how to identify, exploit, and secure endpoints that use default credentials. Let's dive in! 🚀
Default credentials are the factory-set usernames and passwords that come with many software frameworks, applications, and hardware devices. They are intended for the initial setup process, allowing an administrator to log in for the first time without having to go through a complex user creation flow. Common examples include admin/admin, root/password, or test/test.
This vulnerability falls under the broader OWASP Top 10 category A07:2021 - Identification and Authentication Failures. The core issue is that these credentials are well-known and publicly documented. Attackers often use automated tools to scan for systems that still have these defaults enabled.
Default credentials exist primarily to help developers quickly test and set up applications during development. However, they often find their way into production environments due to rushed deployments, poor documentation, or simple oversight. Sometimes, teams intentionally keep them unchanged for "easier maintenance," which creates significant security risks. It's crucial to change all default credentials immediately upon deployment to prevent unauthorized access and protect your application from potential breaches.
Let's look at a code snippet that demonstrates the use of default credentials in an admin panel. This example shows how an attacker might exploit these credentials to gain unauthorized access.
In this code, the admin panel uses hardcoded default credentials (admin and admin123). Let's break down the vulnerabilities:
- Hardcoded Credentials: The
DEFAULT_ADMINdictionary stores the username and password directly in the source code. This is a major security risk, as anyone with access to the code repository can see the credentials. - Weak Authentication Logic: The
/loginendpoint performs a simple string comparison to check the credentials. If they match, it returns a success message that includes"access": "FULL_ADMIN". This response explicitly tells the user how to authenticate for other endpoints. - Insecure Authorization: The
/usersendpoint is particularly vulnerable because it relies on a simple, predictable header check (access == "FULL_ADMIN") for authorization. An attacker who has logged in with default credentials can easily access all user data by including this header in their request. Worse, an attacker who understands the system could potentially skip the login step and directly query the endpoint with the required header.
Now, let's see how an attacker might exploit the vulnerable code using simple curl commands. This two-step process demonstrates how easy it is to gain unauthorized access when default credentials are left unchanged.
Let's analyze the attack:
-
Authentication: The attacker sends a
POSTrequest to the/api/admin/loginendpoint.-H "Content-Type: application/json"tells the server that the request body is in JSON format.-d '{"username": "admin", "password": "admin123"}'provides the default credentials as the payload.- The server responds with a success message, which crucially reveals the key to the next step:
"access":"FULL_ADMIN".
-
Authorization & Data Exfiltration: The attacker now knows how to access protected endpoints. They send a
GETrequest to/api/admin/users.-H "access: FULL_ADMIN"includes the custom header required by the endpoint's insecure authorization check.
The first step to fixing this vulnerability is to stop hardcoding credentials. The best practice is to store secrets like passwords in environment variables, which are kept separate from the application's source code. This prevents them from being accidentally committed to version control systems like Git.
The use of .env files with admin credentials and JWT secrets in this lesson is for demonstration purposes only; never commit real secrets or sensitive credentials to version control.
First, your .env file should look like this:
Note that admin credentials in .env are for bootstrap only; long-term admin users should be stored as hashed records in the database, with rotation and audit logging.
Now, let's update the code to load credentials from these environment variables. We will also add a placeholder for a password verification function, which in a real application should use a strong hashing algorithm like Argon2 or bcrypt.
When choosing credentials for your default admin user, use a complex username that doesn't reveal the admin role (avoid "admin" or "root"), and generate a strong password of at least 16 characters with a mix of uppercase, lowercase, numbers, and special characters.
While moving credentials to environment variables is a good first step, our authorization model is still flawed. Relying on a static, predictable header is not secure. A much better approach is to use a standard, token-based authentication mechanism like JSON Web Tokens (JWT).
A JWT is a compact, digitally signed token that contains "claims" (e.g., user ID, roles, expiration date). When a user logs in, the server generates a JWT and sends it to the client. The client then includes this token in the header of subsequent requests to prove its identity. Because the token is signed, the server can verify its authenticity without needing to store session state.
Here's how you might implement JWT authentication in Python using FastAPI and jose:
In this updated /login endpoint, instead of returning a simple message, we now generate a short-lived JWT. The token's payload contains the user's role, and it's signed with a secret key. This token acts as a temporary, verifiable credential for the user.
Finally, let's secure the /users endpoint by requiring a valid JWT with an admin role. We can achieve this in FastAPI by using dependency injection, creating a chain of verification functions that run before our endpoint logic.
This new implementation is far more secure:
authenticate_token: This dependency extracts the token from theAuthorization: Bearer <token>header. It decodes the JWT, which automatically verifies its signature and checks if it has expired. If the token is invalid in any way, it raises a401 Unauthorizederror.verify_admin_role: This dependency runs afterauthenticate_token. It inspects the payload of the now-validated token to ensure the user has theadminrole. If not, it raises a403 Forbiddenerror.
In this lesson, we explored the risks associated with default credentials and how they can be exploited. We learned how to identify vulnerable endpoints and secure them using environment variables and JWT-based authentication. By implementing these best practices, you can protect your application from unauthorized access and potential breaches.
As you move on to the practice exercises, remember the importance of securing your endpoints and managing credentials properly. Good luck, and see you in the next lesson! 🎉
