Skip to content

Commit f20797b

Browse files
committed
fix(security): validate credentials and prevent weak defaults
Replace hardcoded default credentials with sentinel values and add comprehensive validation to prevent insecure deployments. Changes: - Replace weak defaults in settings.py with CHANGE_ME_IN_ENV_FILE - Add validators for SECRET_KEY, DB_PASSWORD, and DB_USER - Enforce minimum 32-character length for SECRET_KEY - Detect and reject known weak passwords and default values - Update .env.example with clear instructions and secure placeholders - Add comprehensive test coverage for all validators (100% coverage) - Extract MIN_SECRET_KEY_LENGTH constant to avoid magic numbers Security impact: - Prevents accidental production deployment with default credentials - Forces proper configuration before application startup - Provides clear error messages with remediation steps
1 parent 420aefc commit f20797b

File tree

3 files changed

+292
-27
lines changed

3 files changed

+292
-27
lines changed

.env.example

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,11 @@ BASE_URL=http://localhost:8000
1818
# not set, defaults to False.
1919
NO_ROOT_ROUTE=False
2020

21-
# Database Settings These must be changed to match your setup, and the database
22-
# must already exist.
23-
DB_USER=dbuser
24-
DB_PASSWORD=my_secret_passw0rd
21+
# Database Settings - REQUIRED
22+
# These must be changed to match your PostgreSQL setup.
23+
# The database must already exist before running the application.
24+
DB_USER=your_database_username
25+
DB_PASSWORD=your_secure_database_password
2526
DB_ADDRESS=localhost
2627
DB_PORT=5432
2728
DB_NAME=my_database_name
@@ -32,9 +33,12 @@ DB_NAME=my_database_name
3233
# already exist.
3334
TEST_DB_NAME=my_database_name_tests
3435

35-
# generate your own super secret key here, used by the JWT functions.
36-
# 32 characters or longer, definately change the below!!
37-
SECRET_KEY=change_me_to_something_secret
36+
# JWT Secret Key - CRITICAL SECURITY SETTING
37+
# Generate with one of these commands:
38+
# openssl rand -hex 32
39+
# python -c 'import secrets; print(secrets.token_hex(32))'
40+
# Must be 32+ characters. DO NOT use the example below!
41+
SECRET_KEY=CHANGE_ME_generate_with_command_above
3842

3943
# How long the access token is valid for, in minutes. Defaults to 120 (2 hours)
4044
ACCESS_TOKEN_EXPIRE_MINUTES=120
@@ -44,19 +48,23 @@ ACCESS_TOKEN_EXPIRE_MINUTES=120
4448
# If you want all origins to access (the default), use * or comment out:
4549
CORS_ORIGINS=*
4650

47-
# Email Settings specific to your email provider
48-
MAIL_USERNAME=test_username
49-
MAIL_PASSWORD=s3cr3tma1lp@ssw0rd
50-
MAIL_FROM=test@email.com
51+
# Email Settings - OPTIONAL for development, REQUIRED for production
52+
# Set these for password reset and notification emails to work
53+
# Leave empty for local development (email features will be disabled)
54+
MAIL_USERNAME=your_smtp_username
55+
MAIL_PASSWORD=your_smtp_password
56+
MAIL_FROM=noreply@yourdomain.com
5157
MAIL_PORT=587
52-
MAIL_SERVER=mail.server.com
58+
MAIL_SERVER=smtp.yourmailprovider.com
5359
MAIL_FROM_NAME="FastAPI Template"
5460

5561
# Admin Pages Settings
5662
ADMIN_PAGES_ENABLED=True
5763
ADMIN_PAGES_ROUTE=/admin
5864
ADMIN_PAGES_TITLE="API Administration"
59-
ADMIN_PAGES_ENCRYPTION_KEY=change_me_to_a_secret_fernet_key (optional)
65+
# Optional: Generate with: python -c 'from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())'
66+
# Leave empty to auto-generate on startup (regenerates each restart)
67+
ADMIN_PAGES_ENCRYPTION_KEY=
6068
ADMIN_PAGES_TIMEOUT=86400
6169

6270
# Common Email Settings

app/config/settings.py

Lines changed: 102 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,24 @@
2222
logger.error("Please run 'api-admin custom init' to regenerate defaults.")
2323
sys.exit(1)
2424

25+
# Security validation constants
26+
MIN_SECRET_KEY_LENGTH = 32
27+
2528

2629
class Settings(BaseSettings):
2730
"""Main Settings class.
2831
2932
This allows to set some defaults, that can be overwritten from the .env
3033
file if it exists.
31-
Do NOT put passwords and similar in here, use the .env file instead, it will
32-
not be stored in the Git repository.
34+
35+
SECURITY WARNING: Critical settings (SECRET_KEY, DB_PASSWORD, DB_USER)
36+
MUST be set in your .env file. The application will fail to start if
37+
these are not properly configured with secure values.
38+
39+
Do NOT put real passwords in this file - use the .env file instead
40+
(it's in .gitignore and won't be stored in Git).
41+
42+
To get started, copy .env.example to .env and update the values.
3343
"""
3444

3545
project_root: Path = get_project_root()
@@ -45,23 +55,28 @@ class Settings(BaseSettings):
4555
cors_origins: str = "*"
4656

4757
# Setup the Postgresql database.
48-
db_user: str = "my_db_username"
49-
db_password: str = "Sup3rS3cr3tP455w0rd" # noqa: S105
58+
# IMPORTANT: Set DB_USER and DB_PASSWORD in your .env file!
59+
db_user: str = "CHANGE_ME_IN_ENV_FILE"
60+
db_password: str = "CHANGE_ME_IN_ENV_FILE" # noqa: S105
5061
db_address: str = "localhost"
5162
db_port: str = "5432"
5263
db_name: str = "api-template"
5364

5465
test_with_postgres: bool = False
5566

5667
# Setup the TEST Postgresql database.
57-
test_db_user: str = "my_db_username"
58-
test_db_password: str = "Sup3rS3cr3tP455w0rd" # noqa: S105
68+
# Note: Safe defaults for local/CI testing only
69+
test_db_user: str = "test_user"
70+
test_db_password: str = "test_password_local_only" # noqa: S105
5971
test_db_address: str = "localhost"
6072
test_db_port: str = "5432"
6173
test_db_name: str = "api-template-test"
6274

63-
# JTW secret Key
64-
secret_key: str = "32DigitsofSecretNumbers" # noqa: S105
75+
# JWT secret Key - CRITICAL SECURITY SETTING
76+
# Generate with: openssl rand -hex 32
77+
# Or Python: import secrets; secrets.token_hex(32)
78+
# Set SECRET_KEY in your .env file!
79+
secret_key: str = "CHANGE_ME_IN_ENV_FILE" # noqa: S105
6580
access_token_expire_minutes: int = 120
6681

6782
# Custom Metadata
@@ -73,9 +88,11 @@ class Settings(BaseSettings):
7388
year: str = custom_metadata.year
7489

7590
# email settings
76-
mail_username: str = "test_username"
77-
mail_password: str = "s3cr3tma1lp@ssw0rd" # noqa: S105
78-
mail_from: str = "test@email.com"
91+
# Note: Set these in .env for production email functionality
92+
# Email features will fail gracefully if not configured
93+
mail_username: str = ""
94+
mail_password: str = ""
95+
mail_from: str = ""
7996
mail_port: int = 587
8097
mail_server: str = "mail.server.com"
8198
mail_from_name: str = "FASTAPI Template"
@@ -106,6 +123,80 @@ def check_api_root(cls: type[Settings], value: str) -> str:
106123
return value[:-1]
107124
return value
108125

126+
@field_validator("secret_key")
127+
@classmethod
128+
def validate_secret_key(cls: type[Settings], value: str) -> str:
129+
"""Ensure secret key is not a weak or default value."""
130+
weak_keys = [
131+
"CHANGE_ME_IN_ENV_FILE",
132+
"32DigitsofSecretNumbers",
133+
"CHANGE_ME",
134+
"secret",
135+
"secretkey",
136+
]
137+
if value.lower() in [k.lower() for k in weak_keys]:
138+
msg = (
139+
"\n"
140+
"=" * 70 + "\n"
141+
"SECURITY ERROR: SECRET_KEY is using a weak/default value!\n"
142+
"=" * 70 + "\n"
143+
"Generate a strong key with one of these commands:\n"
144+
" openssl rand -hex 32\n"
145+
" python -c 'import secrets; print(secrets.token_hex(32))'\n\n"
146+
"Then add it to your .env file:\n"
147+
" SECRET_KEY=your_generated_key_here\n"
148+
"=" * 70
149+
)
150+
raise ValueError(msg)
151+
if len(value) < MIN_SECRET_KEY_LENGTH:
152+
msg = (
153+
f"SECRET_KEY must be at least {MIN_SECRET_KEY_LENGTH} "
154+
f"characters for security. Current length: {len(value)}"
155+
)
156+
raise ValueError(msg)
157+
return value
158+
159+
@field_validator("db_password")
160+
@classmethod
161+
def validate_db_password(cls: type[Settings], value: str) -> str:
162+
"""Ensure database password is not a weak or default value."""
163+
weak_passwords = [
164+
"CHANGE_ME_IN_ENV_FILE",
165+
"Sup3rS3cr3tP455w0rd",
166+
"CHANGE_ME",
167+
"password",
168+
"admin",
169+
]
170+
if value in weak_passwords:
171+
msg = (
172+
"\n"
173+
"=" * 70 + "\n"
174+
"SECURITY ERROR: DB_PASSWORD is using a weak/default value!\n"
175+
"=" * 70 + "\n"
176+
"Set a strong database password in your .env file:\n"
177+
" DB_PASSWORD=your_secure_password_here\n"
178+
"=" * 70
179+
)
180+
raise ValueError(msg)
181+
return value
182+
183+
@field_validator("db_user")
184+
@classmethod
185+
def validate_db_user(cls: type[Settings], value: str) -> str:
186+
"""Ensure database user is not a default value."""
187+
if value == "CHANGE_ME_IN_ENV_FILE":
188+
msg = (
189+
"\n"
190+
"=" * 70 + "\n"
191+
"CONFIGURATION ERROR: DB_USER is not set!\n"
192+
"=" * 70 + "\n"
193+
"Set your database username in your .env file:\n"
194+
" DB_USER=your_database_username\n"
195+
"=" * 70
196+
)
197+
raise ValueError(msg)
198+
return value
199+
109200

110201
@lru_cache
111202
def get_settings() -> Settings:

0 commit comments

Comments
 (0)