Welcome to the final lesson of the "Cryptographic Failures" course! In our previous lessons, we explored the importance of cryptography in securing data and identified common vulnerabilities, such as weak algorithms and hardcoded secrets.
In this lesson, we'll focus on understanding the limitations of automatic database encryption and the importance of application-level encryption. Let's dive in! 🚀
Many developers assume that enabling database encryption features automatically makes their sensitive data secure. While database encryption protects data at rest (when stored on disk), it doesn't protect data in transit or during processing. When your application queries the database, the data is automatically decrypted and returned in plaintext. This means anyone who can access your application or database through legitimate means can view sensitive data in its unencrypted form.
This automatic decryption creates a significant security risk, especially for sensitive information like credit card numbers, personal identification data, or healthcare records. Let's examine how this endpoint vulnerability manifests in code and learn how to properly secure it using application-level encryption.
Suppose we have an endpoint responsible for adding credit card information. For simplicity, we'll skip JWT authentication in this example.
Here's how the endpoint might look without proper encryption:
This implementation is problematic because it stores the credit card number in its raw form. Even if the database encrypts data at rest, the number is vulnerable during transmission and processing. Additionally, anyone with access to the application can retrieve the unencrypted card numbers.
Let's see how this vulnerability manifests when retrieving data.
The vulnerability becomes even more apparent when retrieving stored card information:
This endpoint retrieves credit card numbers directly from the database. If an attacker gains access to this endpoint or exploits another endpoint via sql injection (we'll cover sql injection in detail next), they can access the exposed information.
Let's see how an attacker might exploit this vulnerability.
An attacker with access to the api can easily retrieve sensitive card information:
As you can see, the credit card numbers are exposed in plaintext in the api response. This vulnerability exists regardless of database - level encryption because the data is automatically decrypted when queried. Let's look at how to properly secure this sensitive data.
Here's how to properly hash and store sensitive data:
This secure implementation hashes the credit card number before storing it. We only store the cardHash (for verification purposes) and the lastFourDigits (for display purposes). This way, even if someone accesses the database directly, they cannot recover the original card number. Let's look at how the hashing function works.
The HashingUtil component uses BCrypt, a strong hashing algorithm specifically designed for passwords and sensitive data:
We use BCrypt because it is specifically designed to be slow and computationally intensive, making it resistant to brute-force attacks. It also automatically handles salt generation and storage, making it a secure choice for hashing sensitive data.
Note: Hashing is a one-way operation - you cannot retrieve the original card number from the hash. In production applications, you would use payment processor tokenization (like Stripe or PayPal) to handle actual card charges. The hash serves to verify cards and detect duplicates without storing the sensitive data itself.
Now, let's see how we can safely retrieve and display card information to users.
When displaying card information to users, we only show the last four digits of the card number:
This implementation ensures that users only see masked card numbers with the last four digits visible. The last four digits are sufficient for users to identify their cards, as this is a standard practice in the payment card industry.
For example, if a user has multiple cards, they can easily recognize that ****-****-****-4561 is their Visa card ending in 4561, while ****-****-****-3789 is their Mastercard ending in 3789. This approach provides a balance between security and usability.
When you need to verify a card number (for example, during a payment transaction), you can compare it with the stored hash using BCrypt's verification method:
This method takes the provided cardNumber and the storedHash, then uses BCrypt's matches functionality to verify if they match. The comparison is done securely, protecting against timing attacks.
If there is an error during comparison, the method returns false to ensure no sensitive information is leaked through error messages.
Here's how to use this verification in a controller endpoint:
To complete the implementation, you'll need the Payment entity model:
And the repository interface:
In this lesson, we explored why relying solely on database encryption is not sufficient for protecting sensitive data. We learned how to implement proper security measures for handling credit card data by:
- Storing only hashed values and the last four digits.
- Never transmitting full card numbers in responses.
- Using BCrypt for secure hashing.
- Displaying masked card numbers with the last four digits for user recognition.
As you move on to the practice exercises, you'll have the opportunity to implement these security methods yourself. In the next lesson, we'll continue to build on this knowledge, further enhancing your application security skills. Keep up the great work! 🌟
