Non-encrypted confidential information
Need
Secure storage, transmission, and encryption of confidential information and credentials
Context
• Usage of Python 3 for developing Python applications
• Usage of Django for building web applications in Python
• Usage of boto3 for interacting with Amazon Web Services (AWS) in Python
• Usage of LDAP for directory services and user authentication
• Usage of firebase_admin for server-side Firebase operations
• Usage of Google Cloud Firestore for managing and storing data in a NoSQL database
Description
1. Non compliant code
# --- Plain-text password storage ---
from django.db import models
from django.contrib.auth.models import User
class UserProfile(models.Model):
username = models.CharField(max_length=200)
password = models.CharField(max_length=200)...**Plain-text password storage and unhashed credential creation.** A Django `UserProfile` model has `username` and `password` fields, and the `password` field is a plain `CharField`. This means passwords are stored as plain text in the database, so if an attacker gains access to the database, they can read every user's password without needing to decrypt anything. The `create_user` view is just as vulnerable: it reads `username` and `password` directly from an HTTP POST request and passes the password, unmodified, to `User.objects.create_user()`. If server logs capture request data, or the connection is not using HTTPS, the credential is exposed both at rest and in transit. **Credit card details stored in plain text.** A `CreditCard` model stores the card number, CVV, expiry date, and cardholder name as plain `CharField`s. This is a direct violation of the PCI DSS (Payment Card Industry Data Security Standard), which requires sensitive cardholder data to be encrypted, and explicitly prohibits storing the CVV post-transaction. **User queries stored in plain text in the database.** A `UserQuery` model stores each user's query as plain text via a `TextField`. If an attacker gains access to the database — whether through a SQL injection attack or a compromised server — they can read every stored query without needing an encryption key. **Hard-coded AWS credentials in source code.** The `aws_access_key_id` and `aws_secret_access_key` are stored as plain-text string literals and passed directly to `boto3.Session()` to create an S3 client. Anyone who gains access to the source code also gains access to the AWS credentials, and with them, unauthorized access to AWS resources. **LDAP service credentials hardcoded in plain text.** The LDAP server URL, bind username, and bind password are stored as plain-text strings and passed directly to `ldap.initialize()` and `simple_bind_s()`. Anyone who can view the code can see these credentials and use them to gain unauthorized access to the LDAP server. **Confidential Firebase data written unencrypted to a local file.** A Firestore `users` collection is fetched and each document's ID and data are written to a local file, `local_data.txt`, in plain text. If an attacker gains physical access to the device and bypasses its security mechanisms, they can read this confidential data directly from the file. **Hard-coded database credentials in `settings.py`.** The Django `DATABASES` setting stores the database name, user, password, host, and port as plain-text string literals directly in the source file. Anyone with access to the source code — including anyone who can view it in version control — has full access to the database credentials. **Hexadecimal encoding used in place of encryption.** A Django view returns confidential information encoded in hexadecimal (`"736563726574696e666f"`, i.e. `"secretinfo"`) as its response body. Hexadecimal is only an encoding, not encryption — it does not provide any real confidentiality, so an attacker who intercepts the response can trivially decode it back to plain text. **Custom `User` model bypassing Django's password hashing.** A custom `User` model stores `username` and `password` as plain `CharField`s instead of using Django's built-in authentication system, so passwords end up stored and compared in plain text rather than being hashed and salted.
2. Steps
• Import the necessary Django libraries for password hashing.
• Replace the CharField for the password with a field that automatically handles password hashing, such as Django's PasswordField, or hash it manually with make_password before saving.
• Ensure that when creating or updating a user, the password is hashed before being stored in the database.
• Verify that the application uses the Django authentication system to check passwords, which will automatically handle the comparison of the hashed password.
• Ensure that the password is not logged or printed anywhere in plain text.
• Use Django's built-in User model and authentication system instead of creating custom user/password models, and never store passwords in plain text — always hash and salt them before storing.
• Consider using additional security measures such as two-factor authentication.
• Use encryption libraries, such as the cryptography package's Fernet or PyCryptodome's AES, to encrypt sensitive data before storing it in the database, and implement a secure key management system to manage the encryption keys.
• Mask the credit card number and CVV when displaying it. Only the last 4 digits of the card number should be visible.
• Ensure that encryption and decryption operations are performed in a secure environment, and use Django's built-in cryptographic fields, such as django_cryptography, to store sensitive information in the database.
• Remove hard-coded credentials (database, AWS, LDAP) from the source code, and instead store them in environment variables or a secure configuration file or secrets vault.
• Use AWS Identity and Access Management (IAM) to create a role with the necessary permissions, attach it to the EC2 instance running the application, and rely on the AWS SDK's default credential provider chain instead of hard-coded keys.
• Encrypt the configuration file or environment variable where LDAP credentials are stored, and ensure the code decrypts them before use.
• Ensure that environment variables holding credentials are not included in version control by adding them to the .gitignore file, and use secure methods such as a password manager to share them with your team.
• Use a secure method to store confidential data on local devices, such as Keychain for iOS, Keystore for Android, or Windows Credentials for Windows, and avoid storing sensitive data on the device when possible — securely delete it as soon as it's no longer needed.
• Store the encryption key securely. The key should not be hard-coded in the application, but instead stored in a secure and encrypted location that only the application can access.
• Implement proper access controls and regularly audit the database and application to restrict and monitor who can view or modify sensitive data.
• Use HTTPS or another secure communication channel for all communications involving sensitive data to prevent interception during transmission.
• Regularly update and patch your systems and encryption libraries to protect against known vulnerabilities.
3. Secure code example
# --- Plain-text password storage (fix) ---
from django.db import models
from django.contrib.auth.models import User
from django.contrib.auth.hashers import make_password
from django.views.decorators.csrf import csrf_protect
class UserProfile(models.Model):...**Plain-text password storage and unhashed credential creation (fix).** The updated code hashes the password via Django's `make_password` before it is ever stored. `UserProfile.save()` is overridden to hash `self.password` before calling the original `save()`, and `create_user` hashes the incoming password before passing it to `User.objects.create_user()`. The view is also protected by `@csrf_protect`. **Credit card details stored in plain text (fix).** The card number and CVV are now encrypted with `Fernet` before being stored, and the corresponding model fields are changed from `CharField` to `BinaryField`. `save()` encrypts both fields before persisting them, and `__str__()` decrypts the card number to show only the last four digits when displaying it. Note that, as written, a fresh random key is generated independently in both `save()` and `__str__()` — a real implementation needs a single, securely-stored key shared by both methods for decryption to succeed. **User queries stored in plain text in the database (fix).** The `query` field is wrapped with `encrypt()` from `django_cryptography.fields`, so every query is automatically encrypted before being written to the database. **Hard-coded AWS credentials in source code (fix).** The hard-coded access key ID and secret access key are removed entirely. `boto3.Session()` is created with no explicit credentials, relying on the AWS SDK's default credential provider chain — such as an IAM role attached to the EC2 instance running the application. **LDAP service credentials hardcoded in plain text (fix).** The LDAP username and password are now retrieved, still encrypted, from the `LDAP_USERNAME` and `LDAP_PASSWORD` environment variables, along with a decryption key from `LDAP_KEY`. A `Fernet` cipher suite built from that key decrypts both values before they are used to bind to the LDAP server. **Confidential Firebase data written unencrypted to a local file (fix).** A random 256-bit AES key is generated, and each fetched document is encrypted with `AES.MODE_EAX` before being written to `local_data.txt`, along with its nonce and authentication tag. Even if an attacker gains physical access to the device, the data cannot be read without the encryption key. **Hard-coded database credentials in `settings.py` (fix).** The database name, user, password, host, and port are read from environment variables via `os.getenv()` instead of being hard-coded, and the variable names are added to `.gitignore` so they are never captured in version control. **Hexadecimal encoding used in place of encryption (fix).** The confidential information is now encrypted with AES (`AES.MODE_EAX`) before being returned, using a randomly generated key and nonce, and the encrypted payload is Base64-encoded for safe transport in the HTTP response. **Custom `User` model bypassing Django's password hashing (fix).** Instead of a custom model with plain-text `username`/`password` fields, the fix uses Django's built-in `User` model — which automatically hashes and salts passwords — and adds an `AuthUserProfile` model with a `OneToOneField` to `User` for any additional profile data.
References
• 020. Non-encrypted confidential information