Initial commit
This commit is contained in:
726
skills/security-patterns/SKILL.md
Normal file
726
skills/security-patterns/SKILL.md
Normal file
@@ -0,0 +1,726 @@
|
||||
---
|
||||
name: security-patterns
|
||||
description: Comprehensive OWASP security guidelines, secure coding patterns, vulnerability prevention strategies, and remediation best practices for building secure applications
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
## Security Patterns Skill
|
||||
|
||||
Provides comprehensive security knowledge based on OWASP Top 10, secure coding practices, common vulnerability patterns, and proven remediation strategies.
|
||||
|
||||
## Core Philosophy: Secure by Default
|
||||
|
||||
**Security is not optional**. Every line of code should be written with security in mind. This skill provides the knowledge to:
|
||||
- Prevent vulnerabilities before they occur
|
||||
- Detect security issues early
|
||||
- Remediate problems effectively
|
||||
- Build security into the development process
|
||||
|
||||
## OWASP Top 10 (2021) - Deep Dive
|
||||
|
||||
### A01: Broken Access Control
|
||||
|
||||
**What It Is**: Failures that allow users to act outside their intended permissions.
|
||||
|
||||
**Common Vulnerabilities**:
|
||||
```python
|
||||
# ❌ INSECURE: No authorization check
|
||||
@app.route('/api/user/<int:user_id>/profile')
|
||||
def get_profile(user_id):
|
||||
user = User.query.get(user_id)
|
||||
return jsonify(user.to_dict())
|
||||
|
||||
# ✅ SECURE: Proper authorization
|
||||
@app.route('/api/user/<int:user_id>/profile')
|
||||
@require_auth
|
||||
def get_profile(user_id):
|
||||
# Check if current user can access this profile
|
||||
if current_user.id != user_id and not current_user.is_admin:
|
||||
abort(403) # Forbidden
|
||||
|
||||
user = User.query.get_or_404(user_id)
|
||||
return jsonify(user.to_dict())
|
||||
```
|
||||
|
||||
**Prevention Strategies**:
|
||||
1. **Deny by Default**: Require explicit permission grants
|
||||
2. **Principle of Least Privilege**: Grant minimum necessary permissions
|
||||
3. **Verify on Server**: Never trust client-side access control
|
||||
4. **Use Mature Frameworks**: Leverage battle-tested authorization libraries
|
||||
5. **Log Access Failures**: Monitor for unauthorized access attempts
|
||||
|
||||
**Testing**:
|
||||
```python
|
||||
def test_authorization():
|
||||
"""Test that users can only access their own data."""
|
||||
# Create two users
|
||||
user1 = create_user()
|
||||
user2 = create_user()
|
||||
|
||||
# User1 tries to access User2's data
|
||||
response = client.get(
|
||||
f'/api/user/{user2.id}/profile',
|
||||
headers={'Authorization': f'Bearer {user1.token}'}
|
||||
)
|
||||
|
||||
assert response.status_code == 403 # Should be forbidden
|
||||
```
|
||||
|
||||
### A02: Cryptographic Failures
|
||||
|
||||
**What It Is**: Failures related to cryptography that expose sensitive data.
|
||||
|
||||
**Secure Patterns**:
|
||||
|
||||
**Password Hashing**:
|
||||
```python
|
||||
# ❌ INSECURE: Weak hashing
|
||||
import hashlib
|
||||
password_hash = hashlib.md5(password.encode()).hexdigest()
|
||||
|
||||
# ✅ SECURE: Strong password hashing
|
||||
import bcrypt
|
||||
|
||||
def hash_password(password: str) -> str:
|
||||
salt = bcrypt.gensalt(rounds=12) # Cost factor 12
|
||||
return bcrypt.hashpw(password.encode('utf-8'), salt).decode('utf-8')
|
||||
|
||||
def verify_password(password: str, hashed: str) -> bool:
|
||||
return bcrypt.checkpw(password.encode('utf-8'), hashed.encode('utf-8'))
|
||||
```
|
||||
|
||||
**Encryption**:
|
||||
```python
|
||||
# ✅ SECURE: AES-256 encryption
|
||||
from cryptography.fernet import Fernet
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2
|
||||
import base64
|
||||
|
||||
def generate_encryption_key(password: str, salt: bytes) -> bytes:
|
||||
"""Generate encryption key from password."""
|
||||
kdf = PBKDF2(
|
||||
algorithm=hashes.SHA256(),
|
||||
length=32,
|
||||
salt=salt,
|
||||
iterations=100000,
|
||||
)
|
||||
return base64.urlsafe_b64encode(kdf.derive(password.encode()))
|
||||
|
||||
def encrypt_data(data: str, key: bytes) -> str:
|
||||
"""Encrypt data using Fernet (AES-128-CBC + HMAC)."""
|
||||
f = Fernet(key)
|
||||
return f.encrypt(data.encode()).decode()
|
||||
|
||||
def decrypt_data(encrypted: str, key: bytes) -> str:
|
||||
"""Decrypt data."""
|
||||
f = Fernet(key)
|
||||
return f.decrypt(encrypted.encode()).decode()
|
||||
```
|
||||
|
||||
**Secure Random**:
|
||||
```python
|
||||
# ❌ INSECURE: Predictable random
|
||||
import random
|
||||
token = str(random.randint(100000, 999999))
|
||||
|
||||
# ✅ SECURE: Cryptographically secure random
|
||||
import secrets
|
||||
|
||||
def generate_secure_token(length: int = 32) -> str:
|
||||
"""Generate cryptographically secure token."""
|
||||
return secrets.token_urlsafe(length)
|
||||
|
||||
def generate_reset_token() -> str:
|
||||
"""Generate password reset token."""
|
||||
return secrets.token_hex(32) # 64 character hex string
|
||||
```
|
||||
|
||||
**Secret Management**:
|
||||
```python
|
||||
# ❌ INSECURE: Hardcoded secrets
|
||||
API_KEY = "sk_live_abcdef123456"
|
||||
DB_PASSWORD = "mysecretpassword"
|
||||
|
||||
# ✅ SECURE: Environment variables
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv() # Load from .env file
|
||||
|
||||
API_KEY = os.environ.get('API_KEY')
|
||||
DB_PASSWORD = os.environ.get('DB_PASSWORD')
|
||||
|
||||
if not API_KEY:
|
||||
raise ValueError("API_KEY environment variable not set")
|
||||
```
|
||||
|
||||
### A03: Injection
|
||||
|
||||
**SQL Injection Prevention**:
|
||||
```python
|
||||
# ❌ INSECURE: String concatenation
|
||||
def get_user_by_username(username):
|
||||
query = f"SELECT * FROM users WHERE username = '{username}'"
|
||||
return db.execute(query)
|
||||
|
||||
# ✅ SECURE: Parameterized queries
|
||||
def get_user_by_username(username):
|
||||
query = "SELECT * FROM users WHERE username = %s"
|
||||
return db.execute(query, (username,))
|
||||
|
||||
# ✅ SECURE: ORM usage
|
||||
def get_user_by_username(username):
|
||||
return User.query.filter_by(username=username).first()
|
||||
```
|
||||
|
||||
**Command Injection Prevention**:
|
||||
```python
|
||||
# ❌ INSECURE: Shell command with user input
|
||||
import os
|
||||
def ping_host(hostname):
|
||||
os.system(f"ping -c 4 {hostname}")
|
||||
|
||||
# ✅ SECURE: Subprocess with list arguments
|
||||
import subprocess
|
||||
def ping_host(hostname):
|
||||
# Validate hostname
|
||||
if not re.match(r'^[a-zA-Z0-9.-]+$', hostname):
|
||||
raise ValueError("Invalid hostname")
|
||||
|
||||
result = subprocess.run(
|
||||
['ping', '-c', '4', hostname],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10
|
||||
)
|
||||
return result.stdout
|
||||
```
|
||||
|
||||
**NoSQL Injection Prevention**:
|
||||
```python
|
||||
# ❌ INSECURE: Direct query construction
|
||||
def find_user(user_id):
|
||||
query = {"_id": user_id} # If user_id is dict, can inject
|
||||
return db.users.find_one(query)
|
||||
|
||||
# ✅ SECURE: Type validation
|
||||
def find_user(user_id):
|
||||
# Ensure user_id is a string
|
||||
if not isinstance(user_id, str):
|
||||
raise TypeError("user_id must be string")
|
||||
|
||||
from bson.objectid import ObjectId
|
||||
try:
|
||||
query = {"_id": ObjectId(user_id)}
|
||||
except:
|
||||
return None
|
||||
|
||||
return db.users.find_one(query)
|
||||
```
|
||||
|
||||
**Template Injection Prevention**:
|
||||
```python
|
||||
# ❌ INSECURE: Rendering user input as template
|
||||
from flask import render_template_string
|
||||
def render_page(template_str):
|
||||
return render_template_string(template_str)
|
||||
|
||||
# ✅ SECURE: Render with automatic escaping
|
||||
from flask import render_template
|
||||
def render_page(data):
|
||||
return render_template('page.html', data=data)
|
||||
# In template: {{ data|e }} or use autoescaping
|
||||
```
|
||||
|
||||
### A04: Insecure Design
|
||||
|
||||
**Secure Design Patterns**:
|
||||
|
||||
**Rate Limiting**:
|
||||
```python
|
||||
from flask_limiter import Limiter
|
||||
from flask_limiter.util import get_remote_address
|
||||
|
||||
limiter = Limiter(
|
||||
app,
|
||||
key_func=get_remote_address,
|
||||
default_limits=["200 per day", "50 per hour"]
|
||||
)
|
||||
|
||||
@app.route('/api/login', methods=['POST'])
|
||||
@limiter.limit("5 per minute") # Prevent brute force
|
||||
def login():
|
||||
# Login logic
|
||||
pass
|
||||
```
|
||||
|
||||
**Business Logic Protection**:
|
||||
```python
|
||||
# ✅ SECURE: Prevent business logic flaws
|
||||
class EcommerceCart:
|
||||
def apply_discount(self, code: str) -> bool:
|
||||
"""Apply discount code with proper validation."""
|
||||
# Validate discount hasn't been used
|
||||
if self.discount_used:
|
||||
raise ValueError("Discount already applied")
|
||||
|
||||
# Validate discount code
|
||||
discount = DiscountCode.query.filter_by(
|
||||
code=code,
|
||||
active=True
|
||||
).first()
|
||||
|
||||
if not discount:
|
||||
return False
|
||||
|
||||
# Check expiration
|
||||
if discount.expires_at < datetime.now():
|
||||
return False
|
||||
|
||||
# Check usage limit
|
||||
if discount.usage_count >= discount.max_uses:
|
||||
return False
|
||||
|
||||
# Check minimum purchase amount
|
||||
if self.total < discount.min_purchase:
|
||||
return False
|
||||
|
||||
# Apply discount
|
||||
self.discount_amount = min(
|
||||
self.total * discount.percentage / 100,
|
||||
discount.max_discount_amount
|
||||
)
|
||||
self.discount_used = True
|
||||
discount.usage_count += 1
|
||||
|
||||
return True
|
||||
```
|
||||
|
||||
### A05: Security Misconfiguration
|
||||
|
||||
**Secure Configuration Checklist**:
|
||||
|
||||
**Security Headers**:
|
||||
```python
|
||||
from flask import Flask
|
||||
from flask_talisman import Talisman
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
# Force HTTPS and set security headers
|
||||
Talisman(app,
|
||||
force_https=True,
|
||||
strict_transport_security=True,
|
||||
strict_transport_security_max_age=31536000,
|
||||
content_security_policy={
|
||||
'default-src': "'self'",
|
||||
'script-src': ["'self'", "'unsafe-inline'"],
|
||||
'style-src': ["'self'", "'unsafe-inline'"],
|
||||
'img-src': ["'self'", "data:", "https:"],
|
||||
},
|
||||
content_security_policy_nonce_in=['script-src'],
|
||||
referrer_policy='strict-origin-when-cross-origin',
|
||||
feature_policy={
|
||||
'geolocation': "'none'",
|
||||
'microphone': "'none'",
|
||||
'camera': "'none'",
|
||||
}
|
||||
)
|
||||
|
||||
@app.after_request
|
||||
def set_security_headers(response):
|
||||
"""Set additional security headers."""
|
||||
response.headers['X-Content-Type-Options'] = 'nosniff'
|
||||
response.headers['X-Frame-Options'] = 'DENY'
|
||||
response.headers['X-XSS-Protection'] = '1; mode=block'
|
||||
response.headers['Permissions-Policy'] = 'geolocation=(), microphone=(), camera=()'
|
||||
return response
|
||||
```
|
||||
|
||||
**CORS Configuration**:
|
||||
```python
|
||||
# ❌ INSECURE: Wildcard CORS
|
||||
from flask_cors import CORS
|
||||
CORS(app, origins="*") # Allows any origin
|
||||
|
||||
# ✅ SECURE: Specific origins
|
||||
CORS(app,
|
||||
origins=["https://yourdomain.com", "https://app.yourdomain.com"],
|
||||
methods=["GET", "POST"],
|
||||
allow_headers=["Content-Type", "Authorization"],
|
||||
max_age=3600,
|
||||
supports_credentials=True
|
||||
)
|
||||
```
|
||||
|
||||
**Error Handling**:
|
||||
```python
|
||||
# ❌ INSECURE: Verbose error messages
|
||||
@app.errorhandler(Exception)
|
||||
def handle_error(error):
|
||||
return jsonify({
|
||||
"error": str(error),
|
||||
"traceback": traceback.format_exc()
|
||||
}), 500
|
||||
|
||||
# ✅ SECURE: Generic error messages
|
||||
@app.errorhandler(Exception)
|
||||
def handle_error(error):
|
||||
# Log full error for debugging
|
||||
app.logger.error(f"Error: {error}", exc_info=True)
|
||||
|
||||
# Return generic message to user
|
||||
return jsonify({
|
||||
"error": "An internal error occurred",
|
||||
"request_id": generate_request_id()
|
||||
}), 500
|
||||
```
|
||||
|
||||
### A06: Vulnerable Components
|
||||
|
||||
**Dependency Management**:
|
||||
```python
|
||||
# requirements.txt - Pin versions
|
||||
flask==2.3.0
|
||||
requests==2.31.0
|
||||
cryptography==41.0.0
|
||||
|
||||
# Use pip-audit or safety
|
||||
$ pip-audit # Check for vulnerabilities
|
||||
$ safety check # Alternative tool
|
||||
```
|
||||
|
||||
**Automated Scanning**:
|
||||
```yaml
|
||||
# .github/workflows/security.yml
|
||||
name: Security Scan
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
security:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Run pip-audit
|
||||
run: |
|
||||
pip install pip-audit
|
||||
pip-audit -r requirements.txt
|
||||
```
|
||||
|
||||
### A07: Authentication Failures
|
||||
|
||||
**Secure Authentication Pattern**:
|
||||
```python
|
||||
from werkzeug.security import check_password_hash
|
||||
import secrets
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
class SecureAuth:
|
||||
# Password policy
|
||||
MIN_PASSWORD_LENGTH = 12
|
||||
REQUIRE_UPPERCASE = True
|
||||
REQUIRE_LOWERCASE = True
|
||||
REQUIRE_DIGIT = True
|
||||
REQUIRE_SPECIAL = True
|
||||
|
||||
# Account lockout
|
||||
MAX_LOGIN_ATTEMPTS = 5
|
||||
LOCKOUT_DURATION = timedelta(minutes=15)
|
||||
|
||||
# Session security
|
||||
SESSION_TIMEOUT = timedelta(hours=2)
|
||||
SESSION_ABSOLUTE_TIMEOUT = timedelta(hours=8)
|
||||
|
||||
@staticmethod
|
||||
def validate_password_strength(password: str) -> Tuple[bool, str]:
|
||||
"""Validate password meets security requirements."""
|
||||
if len(password) < SecureAuth.MIN_PASSWORD_LENGTH:
|
||||
return False, f"Password must be at least {SecureAuth.MIN_PASSWORD_LENGTH} characters"
|
||||
|
||||
if SecureAuth.REQUIRE_UPPERCASE and not any(c.isupper() for c in password):
|
||||
return False, "Password must contain uppercase letter"
|
||||
|
||||
if SecureAuth.REQUIRE_LOWERCASE and not any(c.islower() for c in password):
|
||||
return False, "Password must contain lowercase letter"
|
||||
|
||||
if SecureAuth.REQUIRE_DIGIT and not any(c.isdigit() for c in password):
|
||||
return False, "Password must contain digit"
|
||||
|
||||
if SecureAuth.REQUIRE_SPECIAL and not any(c in "!@#$%^&*" for c in password):
|
||||
return False, "Password must contain special character"
|
||||
|
||||
return True, "Password meets requirements"
|
||||
|
||||
@staticmethod
|
||||
def login(username: str, password: str) -> dict:
|
||||
"""Secure login implementation."""
|
||||
user = User.query.filter_by(username=username).first()
|
||||
|
||||
# Timing attack prevention: always hash even if user doesn't exist
|
||||
if not user:
|
||||
check_password_hash("$2b$12$dummy", password)
|
||||
return {"success": False, "message": "Invalid credentials"}
|
||||
|
||||
# Check if account is locked
|
||||
if user.locked_until and user.locked_until > datetime.now():
|
||||
return {"success": False, "message": "Account temporarily locked"}
|
||||
|
||||
# Verify password
|
||||
if not check_password_hash(user.password_hash, password):
|
||||
user.failed_login_attempts += 1
|
||||
|
||||
# Lock account after max attempts
|
||||
if user.failed_login_attempts >= SecureAuth.MAX_LOGIN_ATTEMPTS:
|
||||
user.locked_until = datetime.now() + SecureAuth.LOCKOUT_DURATION
|
||||
|
||||
db.session.commit()
|
||||
return {"success": False, "message": "Invalid credentials"}
|
||||
|
||||
# Reset failed attempts on successful login
|
||||
user.failed_login_attempts = 0
|
||||
user.last_login = datetime.now()
|
||||
db.session.commit()
|
||||
|
||||
# Create session
|
||||
session_token = secrets.token_urlsafe(32)
|
||||
session = UserSession(
|
||||
user_id=user.id,
|
||||
token=session_token,
|
||||
expires_at=datetime.now() + SecureAuth.SESSION_TIMEOUT,
|
||||
absolute_expires_at=datetime.now() + SecureAuth.SESSION_ABSOLUTE_TIMEOUT
|
||||
)
|
||||
db.session.add(session)
|
||||
db.session.commit()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"token": session_token,
|
||||
"expires_in": int(SecureAuth.SESSION_TIMEOUT.total_seconds())
|
||||
}
|
||||
```
|
||||
|
||||
**Multi-Factor Authentication**:
|
||||
```python
|
||||
import pyotp
|
||||
|
||||
class MFAManager:
|
||||
@staticmethod
|
||||
def generate_secret() -> str:
|
||||
"""Generate TOTP secret for user."""
|
||||
return pyotp.random_base32()
|
||||
|
||||
@staticmethod
|
||||
def get_totp_uri(secret: str, username: str, issuer: str) -> str:
|
||||
"""Generate QR code URI for TOTP app."""
|
||||
totp = pyotp.TOTP(secret)
|
||||
return totp.provisioning_uri(
|
||||
name=username,
|
||||
issuer_name=issuer
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def verify_totp(secret: str, token: str, window: int = 1) -> bool:
|
||||
"""Verify TOTP token with tolerance window."""
|
||||
totp = pyotp.TOTP(secret)
|
||||
return totp.verify(token, valid_window=window)
|
||||
|
||||
@staticmethod
|
||||
def generate_backup_codes(count: int = 10) -> List[str]:
|
||||
"""Generate one-time backup codes."""
|
||||
return [secrets.token_hex(4) for _ in range(count)]
|
||||
```
|
||||
|
||||
### A08: Software and Data Integrity Failures
|
||||
|
||||
**Secure Deserialization**:
|
||||
```python
|
||||
# ❌ INSECURE: pickle allows code execution
|
||||
import pickle
|
||||
def load_data(data):
|
||||
return pickle.loads(data)
|
||||
|
||||
# ✅ SECURE: Use JSON or safer formats
|
||||
import json
|
||||
def load_data(data):
|
||||
return json.loads(data)
|
||||
|
||||
# If you must use pickle, sign the data
|
||||
import hmac
|
||||
import hashlib
|
||||
|
||||
def secure_pickle_dumps(obj, secret_key):
|
||||
"""Pickle with HMAC signature."""
|
||||
pickled = pickle.dumps(obj)
|
||||
signature = hmac.new(secret_key, pickled, hashlib.sha256).hexdigest()
|
||||
return signature.encode() + b':' + pickled
|
||||
|
||||
def secure_pickle_loads(data, secret_key):
|
||||
"""Verify signature before unpickling."""
|
||||
signature, pickled = data.split(b':', 1)
|
||||
expected_signature = hmac.new(secret_key, pickled, hashlib.sha256).hexdigest().encode()
|
||||
|
||||
if not hmac.compare_digest(signature, expected_signature):
|
||||
raise ValueError("Invalid signature")
|
||||
|
||||
return pickle.loads(pickled)
|
||||
```
|
||||
|
||||
### A09: Logging and Monitoring
|
||||
|
||||
**Secure Logging Pattern**:
|
||||
```python
|
||||
import logging
|
||||
from logging.handlers import RotatingFileHandler
|
||||
import json
|
||||
|
||||
# Configure security event logging
|
||||
security_logger = logging.getLogger('security')
|
||||
security_logger.setLevel(logging.INFO)
|
||||
|
||||
handler = RotatingFileHandler(
|
||||
'logs/security.log',
|
||||
maxBytes=10485760, # 10MB
|
||||
backupCount=10
|
||||
)
|
||||
|
||||
formatter = logging.Formatter(
|
||||
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
handler.setFormatter(formatter)
|
||||
security_logger.addHandler(handler)
|
||||
|
||||
def log_security_event(event_type: str, user_id: str, details: dict):
|
||||
"""Log security-relevant events."""
|
||||
event = {
|
||||
"event_type": event_type,
|
||||
"user_id": user_id,
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"details": details,
|
||||
"ip_address": request.remote_addr if request else None
|
||||
}
|
||||
|
||||
security_logger.info(json.dumps(event))
|
||||
|
||||
# Usage
|
||||
log_security_event("LOGIN_SUCCESS", user.id, {"username": user.username})
|
||||
log_security_event("ACCESS_DENIED", user.id, {"resource": "/admin/users"})
|
||||
log_security_event("PASSWORD_CHANGE", user.id, {})
|
||||
```
|
||||
|
||||
### A10: Server-Side Request Forgery (SSRF)
|
||||
|
||||
**SSRF Prevention**:
|
||||
```python
|
||||
import requests
|
||||
from urllib.parse import urlparse
|
||||
|
||||
ALLOWED_PROTOCOLS = ['http', 'https']
|
||||
BLOCKED_IPS = [
|
||||
'127.0.0.0/8', # Loopback
|
||||
'10.0.0.0/8', # Private
|
||||
'172.16.0.0/12', # Private
|
||||
'192.168.0.0/16', # Private
|
||||
'169.254.0.0/16', # Link-local
|
||||
]
|
||||
|
||||
def is_safe_url(url: str) -> bool:
|
||||
"""Validate URL is safe from SSRF."""
|
||||
parsed = urlparse(url)
|
||||
|
||||
# Check protocol
|
||||
if parsed.scheme not in ALLOWED_PROTOCOLS:
|
||||
return False
|
||||
|
||||
# Check for localhost/internal IPs
|
||||
hostname = parsed.hostname
|
||||
if not hostname:
|
||||
return False
|
||||
|
||||
if hostname in ['localhost', '127.0.0.1', '0.0.0.0']:
|
||||
return False
|
||||
|
||||
# Resolve and check IP
|
||||
import socket
|
||||
try:
|
||||
ip = socket.gethostbyname(hostname)
|
||||
|
||||
import ipaddress
|
||||
ip_obj = ipaddress.ip_address(ip)
|
||||
|
||||
# Check if private/internal
|
||||
if ip_obj.is_private or ip_obj.is_loopback:
|
||||
return False
|
||||
|
||||
except:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def fetch_url(url: str) -> str:
|
||||
"""Safely fetch URL content."""
|
||||
if not is_safe_url(url):
|
||||
raise ValueError("URL not allowed")
|
||||
|
||||
response = requests.get(
|
||||
url,
|
||||
timeout=5,
|
||||
allow_redirects=False # Prevent redirect to internal URLs
|
||||
)
|
||||
|
||||
return response.text
|
||||
```
|
||||
|
||||
## Secure Coding Checklist
|
||||
|
||||
### Input Validation
|
||||
- [ ] All user input is validated
|
||||
- [ ] Whitelist validation where possible
|
||||
- [ ] Length limits enforced
|
||||
- [ ] Type checking implemented
|
||||
- [ ] Special characters handled
|
||||
|
||||
### Authentication
|
||||
- [ ] Strong password policy enforced
|
||||
- [ ] Multi-factor authentication available
|
||||
- [ ] Account lockout after failed attempts
|
||||
- [ ] Secure password reset process
|
||||
- [ ] Session timeout configured
|
||||
|
||||
### Authorization
|
||||
- [ ] All endpoints require authorization
|
||||
- [ ] Principle of least privilege applied
|
||||
- [ ] Authorization checked on server-side
|
||||
- [ ] No IDOR vulnerabilities
|
||||
- [ ] Admin functions protected
|
||||
|
||||
### Cryptography
|
||||
- [ ] Strong algorithms used (AES-256, SHA-256)
|
||||
- [ ] No hardcoded secrets
|
||||
- [ ] Secure random for tokens
|
||||
- [ ] TLS/HTTPS enforced
|
||||
- [ ] Passwords hashed with bcrypt/argon2
|
||||
|
||||
### Data Protection
|
||||
- [ ] Sensitive data encrypted at rest
|
||||
- [ ] Sensitive data encrypted in transit
|
||||
- [ ] PII properly handled
|
||||
- [ ] Data retention policies implemented
|
||||
- [ ] Secure deletion procedures
|
||||
|
||||
### Error Handling
|
||||
- [ ] Generic error messages to users
|
||||
- [ ] Detailed errors logged securely
|
||||
- [ ] No stack traces exposed
|
||||
- [ ] Sensitive data not in logs
|
||||
- [ ] Error monitoring implemented
|
||||
|
||||
### Logging & Monitoring
|
||||
- [ ] Security events logged
|
||||
- [ ] Log tampering prevented
|
||||
- [ ] Anomaly detection configured
|
||||
- [ ] Alerting for critical events
|
||||
- [ ] Regular log review
|
||||
|
||||
This skill provides the foundation for writing secure code and identifying vulnerabilities effectively.
|
||||
Reference in New Issue
Block a user