Introduction

Welcome to the very first lesson of the security misconfiguration course! In this lesson, we will 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 will learn how to identify, exploit, and secure endpoints that use default credentials. Let's dive in! 🚀

Understanding Default Credentials

Default credentials are the factory-set usernames and passwords that come with many applications and devices. They are intended for initial setup but can become a security risk if not changed. For example, an admin panel might come with a default username admin and password admin123. If these credentials remain unchanged, anyone with access to the application can log in and potentially access sensitive data.

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 is crucial to change default credentials to prevent unauthorized access and protect your application from potential breaches.

Vulnerable Code Example

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.

@RestController
@RequestMapping("/api/admin")
public class AdminController {
    
    // Default admin credentials (NEVER do this in production)
    private static final String DEFAULT_USERNAME = "admin";
    private static final String DEFAULT_PASSWORD = "admin123";

    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) {
        String username = loginRequest.username();
        String password = loginRequest.password();

        // If default credentials are still active, an attacker can log in
        if (DEFAULT_USERNAME.equals(username) && DEFAULT_PASSWORD.equals(password)) {
            Map<String, String> response = new HashMap<>();
            response.put("message", "Login successful");
            response.put("access", "FULL_ADMIN");
            return ResponseEntity.ok(response);
        }

        return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                .body(Map.of("error", "Unauthorized"));
    }
    
    public record LoginRequest(String username, String password) {}
}

In this code, the admin panel uses default credentials (admin and admin123). If these credentials are not changed, anyone can log in as an admin, posing a significant security risk. This vulnerability can be exploited to access sensitive data or perform unauthorized actions.

Let's look at another vulnerable endpoint that demonstrates how an attacker can exploit default credentials to access sensitive user data:

@GetMapping("/users")
public ResponseEntity<?> getUsers(@RequestHeader(value = "access", required = false) String access) {
    // If attacker logs in using default credentials, they can dump user data
    if ("FULL_ADMIN".equals(access)) {
        List<Map<String, Object>> users = Arrays.asList(
            Map.of("id", 1, "name", "Alice"),
            Map.of("id", 2, "name", "Bob")
        );
        return ResponseEntity.ok(users);
    }

    return ResponseEntity.status(HttpStatus.FORBIDDEN)
            .body(Map.of("error", "Forbidden"));
}

This endpoint is particularly vulnerable because it relies on a simple header check for authentication. An attacker who has logged in with default credentials can easily access all user data by including the FULL_ADMIN access header in their request.

Exploiting the Vulnerability

Now, let's see how an attacker might exploit the vulnerable code using a simple curl command. This demonstration will show how easy it is to gain unauthorized access when default credentials are left unchanged.

# Step 1: Login with default credentials
curl -X POST http://localhost:8080/api/admin/login \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"admin123"}'

# Response: {"message":"Login successful","access":"FULL_ADMIN"}

# Step 2: Access protected endpoint with the access header
curl -X GET http://localhost:8080/api/admin/users \
  -H "access: FULL_ADMIN"

# Response: [{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}]

In this example, the attacker uses a curl command to send a POST request to the admin login endpoint with the default credentials. If the credentials are still active, the attacker will receive a response indicating a successful login, granting them access to the admin panel. They can then use the access token to retrieve sensitive user data.

Implementing Secure Admin User Creation

Let's look at how to properly set up admin users using environment variables and secure password hashing. We'll use Spring Boot's configuration system to manage admin credentials securely.

First, let's create our User entity:

@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    @Column(unique = true, nullable = false)
    private String username;

    @Column(nullable = false)
    private String password;

    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private Role role;

    // Constructors, getters, and setters
    public User() {}

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public Role getRole() {
        return role;
    }

    public void setRole(Role role) {
        this.role = role;
    }
}

Here's the Role enum:

public enum Role {
    USER,
    ADMIN
}

Now, let's create a repository for database operations:

@Repository
public interface UserRepository extends JpaRepository<User, Integer> {
    Optional<User> findByUsername(String username);
}

Next, we will implement the initialization logic using Spring Boot's ApplicationRunner:

@Component
public class DataInitializer implements ApplicationRunner {
    private final UserRepository users;
    private final PasswordEncoder encoder;
    
    @Value("${app.admin.username:}")
    private String adminUsername;
    
    @Value("${app.admin.password:}")
    private String adminPassword;

    public DataInitializer(UserRepository users, PasswordEncoder encoder) {
        this.users = users;
        this.encoder = encoder;
    }

    @Override
    public void run(ApplicationArguments args) {
        try {
            if (adminUsername == null || adminUsername.isBlank() ||
                adminPassword == null || adminPassword.isBlank()) {
                throw new IllegalStateException("Missing admin credentials in configuration");
            }

            String hashedPassword = encoder.encode(adminPassword);
            
            users.findByUsername(adminUsername).orElseGet(() -> {
                User u = new User();
                u.setUsername(adminUsername);
                u.setPassword(hashedPassword);
                u.setRole(Role.ADMIN);
                return users.save(u);
            });
            
            System.out.println("Admin user created/verified successfully");
        } catch (Exception e) {
            System.err.println("Error initializing admin user: " + e.getMessage());
        }
    }
}

Here's how your application.yml file should look:

server:
  port: 3001

spring:
  datasource:
    url: jdbc:sqlite:database.sqlite
    driver-class-name: org.sqlite.JDBC
  jpa:
    database-platform: org.hibernate.community.dialect.SQLiteDialect
    hibernate:
      ddl-auto: update

app:
  jwt:
    secret: your-very-long-and-very-secure-jwt-secret-key
  admin:
    username: sys_admin_acme
    password: X2k9#mP$vL5@qR8n

⚠️ Important Security Note: The credentials shown above (sys_admin_acme and X2k9#mP$vL5@qR8n) are example values for demonstration purposes only. In a real application, never commit actual credentials to version control. Instead, use environment variables, secure secret management systems (like HashiCorp Vault, AWS Secrets Manager, or Azure Key Vault), or encrypted configuration files that are excluded from version control via .gitignore.

You'll also need to configure a PasswordEncoder bean:

@Configuration
public class SecurityConfiguration {
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

When choosing credentials for your default admin user, use a complex username that does not reveal the admin role (avoid admin or root), and generate a strong password of at least 16 characters with a mix of uppercase letters, lowercase letters, numbers, and special characters.

Remember to change these credentials immediately after the first deployment to production.

Implementing JWT-based Authentication

Next, we will implement jwt-based authentication to protect the admin panel. This ensures that only authorized users can access the admin panel.

First, let's create a utility class for JWT operations using the Auth0 JWT library:

@Component
public class JwtUtil {
    private final Algorithm algorithm;
    private final JWTVerifier verifier;

    public JwtUtil(@Value("${app.jwt.secret}") String secret) {
        this.algorithm = Algorithm.HMAC256(secret);
        this.verifier = JWT.require(algorithm).build();
    }

    public String generateTokenWithUserId(Integer userId) {
        return JWT.create()
                .withClaim("userId", userId)
                .sign(algorithm);
    }

    // Generates a JWT token containing the user's ID and role
    // The token expires after 1 hour to limit the window of exposure if compromised
    public String generateTokenWithRole(Integer userId, String role) {
        return JWT.create()
                .withClaim("userId", userId)
                .withClaim("role", role)
                .withExpiresAt(Instant.now().plus(1, ChronoUnit.HOURS))
                .sign(algorithm);
    }

    public DecodedJWT verify(String token) {
        return verifier.verify(token);
    }
}

Now, let's update our login endpoint to use JWT:

@RestController
@RequestMapping("/api/admin")
public class AdminController {
    
    private final UserRepository users;
    private final JwtUtil jwt;
    private final PasswordEncoder passwordEncoder;

    public AdminController(UserRepository users, JwtUtil jwt, PasswordEncoder passwordEncoder) {
        this.users = users;
        this.jwt = jwt;
        this.passwordEncoder = passwordEncoder;
    }

    private ResponseEntity<ErrorResponse> error(HttpStatus status, String detail) {
        return ResponseEntity.status(status).body(new ErrorResponse(detail));
    }

    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody LoginRequest request) {
        Optional<User> adminUser = users.findByUsername(request.username());
        
        if (adminUser.isEmpty()) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                .body(Map.of("error", "Unauthorized"));
        }

        boolean isValidPassword = passwordEncoder.matches(
            request.password(), 
            adminUser.get().getPassword()
        );

        if (isValidPassword) {
            String token = jwt.generateTokenWithRole(
                adminUser.get().getId(),
                adminUser.get().getRole().toString()
            );
            return ResponseEntity.ok(Map.of("token", token));
        }

        return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
            .body(Map.of("error", "Unauthorized"));
    }

    public record LoginRequest(String username, String password) {}
}

Here's the ErrorResponse class for consistent error handling:

public class ErrorResponse {
    private String error;

    public ErrorResponse(String error) {
        this.error = error;
    }

    public String getError() {
        return error;
    }

    public void setError(String error) {
        this.error = error;
    }
}

In this code, we use JWT to generate a token for authenticated users. Instead of our previous simple credential check, we now use BCrypt to securely compare passwords and issue a short-lived JWT token. The token is signed with a secret key and includes an expiration time of 1 hour, ensuring that even if compromised, the token can only be used for a limited time.

Now that we have our authentication mechanism in place, let's see how we can use it to secure our user data endpoint.

Securing the User Data Endpoint

Let's look at how we can protect sensitive endpoints using our JWT authentication. We'll create a method to verify admin access:

private AdminOutcome verifyAdminOrError(String authorizationHeader) {
    if (authorizationHeader == null || authorizationHeader.isBlank()) {
        return new AdminOutcome(null, error(HttpStatus.UNAUTHORIZED, "Missing authorization header"));
    }
    
    String token = authorizationHeader.startsWith("Bearer ") 
        ? authorizationHeader.substring(7) 
        : authorizationHeader;
    
    try {
        DecodedJWT decoded = jwt.verify(token);
        Integer userId = decoded.getClaim("userId").asInt();
        var userOpt = users.findById(userId);
        
        if (userOpt.isEmpty()) {
            return new AdminOutcome(null, error(HttpStatus.UNAUTHORIZED, "User not found"));
        }
        
        User u = userOpt.get();
        if (u.getRole() != Role.ADMIN) {
            return new AdminOutcome(null, error(HttpStatus.FORBIDDEN, "Admin access required"));
        }
        
        return new AdminOutcome(u, null);
    } catch (Exception e) {
        return new AdminOutcome(null, error(HttpStatus.FORBIDDEN, "Admin access required"));
    }
}

private record AdminOutcome(User user, ResponseEntity<ErrorResponse> error) {}

Now, let's update our user data endpoint to use this verification method:

@GetMapping("/users")
public ResponseEntity<?> getUsers(
        @RequestHeader(value = "authorization", required = false) String authorization) {
    
    var outcome = verifyAdminOrError(authorization);
    if (outcome.error() != null) {
        return outcome.error();
    }

    List<Map<String, Object>> usersList = users.findAll().stream()
        .map(u -> Map.<String, Object>of(
            "id", u.getId(),
            "username", u.getUsername(),
            "role", u.getRole().toString()
        ))
        .toList();

    return ResponseEntity.ok(Map.of("users", usersList));
}

This version uses the verifyAdminOrError method to verify the JWT token from the Authorization header and ensure that only users with an ADMIN role can access sensitive user information. The method returns an AdminOutcome record that contains either the authenticated user or an error response, making it easy to handle both success and failure cases.

Here's a complete example of how to test the secured endpoint:

# Step 1: Login with proper credentials from configuration
curl -X POST http://localhost:3001/api/admin/login \
  -H "Content-Type: application/json" \
  -d '{"username":"sys_admin_acme","password":"X2k9#mP$vL5@qR8n"}'

# Response: {"token":"eyJ0eXAiOiJKV1QiLCJhbGc..."}

# Step 2: Access protected endpoint with the JWT token
curl -X GET http://localhost:3001/api/admin/users \
  -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGc..."

# Response: {"users":[{"id":1,"username":"sys_admin_acme","role":"ADMIN"}]}

# Step 3: Try accessing without a token (should fail)
curl -X GET http://localhost:3001/api/admin/users

# Response: {"error":"Missing authorization header"}

With these security measures in place, we have significantly improved our application's security posture. Let's wrap up what we have learned and look at what is coming next.

Conclusion and Next Steps

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.

Key takeaways from this lesson:

  1. Never hardcode credentials - Always use environment variables or secure configuration management
  2. Use strong password hashing - BCrypt provides secure one-way hashing for passwords
  3. Implement JWT tokens - Short-lived tokens with expiration times limit the damage from compromised credentials
  4. Verify admin roles - Always check user roles before granting access to sensitive operations
  5. Choose strong credentials - Use complex usernames and passwords with proper entropy

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! 🎉

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal