add security tooling and development workflow

- ruff for linting and formatting
- bandit for security scanning
- mypy for type checking
- pip-audit for dependency vulnerabilities
- Makefile with lint/format/security/test targets
This commit is contained in:
Username
2025-12-20 17:20:21 +01:00
parent 4e38517faf
commit adbb5be5c0
4 changed files with 484 additions and 0 deletions

85
Makefile Normal file
View File

@@ -0,0 +1,85 @@
# FlaskPaste Development Makefile
# Usage: make <target>
.PHONY: help install dev lint format security test check clean
PYTHON := python3
VENV := ./venv
PIP := $(VENV)/bin/pip
PYTEST := $(VENV)/bin/pytest
RUFF := $(VENV)/bin/ruff
MYPY := $(VENV)/bin/mypy
BANDIT := $(VENV)/bin/bandit
PIP_AUDIT := $(VENV)/bin/pip-audit
# Default target
help:
@echo "FlaskPaste Development Commands"
@echo "────────────────────────────────"
@echo " make install Install production dependencies"
@echo " make dev Install dev dependencies"
@echo " make lint Run ruff linter"
@echo " make format Format code with ruff"
@echo " make types Run mypy type checker"
@echo " make security Run bandit + pip-audit"
@echo " make test Run pytest"
@echo " make check Run all checks (lint + security + test)"
@echo " make clean Remove cache files"
# Setup
install:
$(PIP) install -r requirements.txt
dev:
$(PIP) install -r requirements.txt -r requirements-dev.txt
# Code quality
lint:
$(RUFF) check app/ tests/ fpaste
format:
$(RUFF) format app/ tests/ fpaste
$(RUFF) check --fix app/ tests/ fpaste
types:
$(MYPY) app/ --ignore-missing-imports
# Security
security: security-code security-deps
security-code:
@echo "── Bandit (code security) ──"
$(BANDIT) -r app/ -ll -q
security-deps:
@echo "── pip-audit (dependency vulnerabilities) ──"
$(PIP_AUDIT) --strict --progress-spinner=off || true
# Testing
test:
$(PYTEST) tests/ -v --tb=short
test-cov:
$(PYTEST) tests/ -v --tb=short --cov=app --cov-report=term-missing
# Combined checks
check: lint types security test
@echo ""
@echo "✓ All checks passed"
# CI target (non-interactive, strict)
ci:
$(RUFF) check app/ tests/ fpaste
$(MYPY) app/ --ignore-missing-imports
$(BANDIT) -r app/ -ll -q
$(PIP_AUDIT) --strict --progress-spinner=off
$(PYTEST) tests/ -v --tb=short
# Cleanup
clean:
find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
find . -type d -name .pytest_cache -exec rm -rf {} + 2>/dev/null || true
find . -type d -name .mypy_cache -exec rm -rf {} + 2>/dev/null || true
find . -type d -name .ruff_cache -exec rm -rf {} + 2>/dev/null || true
find . -type f -name "*.pyc" -delete 2>/dev/null || true
rm -rf .coverage htmlcov/ 2>/dev/null || true

265
SECURITY.md Normal file
View File

@@ -0,0 +1,265 @@
# Security
FlaskPaste is designed with security as a primary concern. This document describes security features, best practices, and responsible disclosure procedures.
## Security Features
### Authentication
**Client Certificate Authentication (mTLS)**
FlaskPaste supports mutual TLS authentication via reverse proxy:
1. Reverse proxy terminates TLS and validates client certificates
2. Proxy extracts SHA1 fingerprint and passes via `X-SSL-Client-SHA1` header
3. FlaskPaste uses fingerprint for ownership tracking and authorization
```nginx
# nginx example
location / {
proxy_pass http://127.0.0.1:5000;
proxy_set_header X-SSL-Client-SHA1 $ssl_client_fingerprint;
}
```
**Proxy Trust Validation**
Defense-in-depth against header spoofing:
```bash
export FLASKPASTE_PROXY_SECRET="shared-secret"
```
The proxy must send `X-Proxy-Secret` header; requests without valid secret are treated as anonymous.
### Password Protection
Pastes can be password-protected using PBKDF2-HMAC-SHA256:
- 600,000 iterations (OWASP 2023 recommendation)
- 32-byte random salt per password
- Constant-time comparison prevents timing attacks
- Passwords never logged or stored in plaintext
```bash
# Create protected paste
curl -H "X-Paste-Password: secret" --data-binary @file.txt https://paste.example.com/
# Access protected paste
curl -H "X-Paste-Password: secret" https://paste.example.com/abc123/raw
```
### End-to-End Encryption
CLI supports client-side AES-256-GCM encryption:
- Key generated locally, never sent to server
- Key appended to URL as fragment (`#key`) which browsers never transmit
- Server stores only opaque ciphertext
- Zero-knowledge: server cannot read content
```bash
./fpaste create -e secret.txt
# Returns: https://paste.example.com/abc123#<base64-key>
```
### Abuse Prevention
**Content-Hash Deduplication**
Prevents spam flooding by throttling repeated identical submissions:
```bash
FLASKPASTE_DEDUP_WINDOW=3600 # 1 hour window
FLASKPASTE_DEDUP_MAX=3 # Max 3 identical submissions
```
**Proof-of-Work**
Computational puzzle prevents automated spam:
```bash
FLASKPASTE_POW_DIFFICULTY=20 # Leading zero bits required
FLASKPASTE_POW_TTL=300 # Challenge validity (seconds)
```
**Entropy Enforcement**
Optional minimum entropy requirement to enforce encrypted uploads:
```bash
FLASKPASTE_MIN_ENTROPY=6.0 # Bits per byte (encrypted ~7.5-8.0)
FLASKPASTE_MIN_ENTROPY_SIZE=256 # Only check content >= this size
```
### Security Headers
All responses include:
| Header | Value |
|--------|-------|
| `Strict-Transport-Security` | `max-age=31536000; includeSubDomains` |
| `X-Content-Type-Options` | `nosniff` |
| `X-Frame-Options` | `DENY` |
| `X-XSS-Protection` | `1; mode=block` |
| `Content-Security-Policy` | `default-src 'none'` |
| `Referrer-Policy` | `no-referrer` |
| `Permissions-Policy` | `geolocation=(), microphone=(), camera=()` |
| `Cache-Control` | `no-store, no-cache, must-revalidate` |
| `Pragma` | `no-cache` |
### Request Tracing
All requests receive `X-Request-ID` header for log correlation and debugging. Pass your own ID to trace requests through reverse proxies.
## Input Validation
### Paste IDs
- Hexadecimal only (`[a-f0-9]+`)
- Configurable length (default 12 characters)
- Validated on all endpoints
### MIME Types
- Magic byte detection for binary formats
- Sanitized against injection
- Only safe characters allowed: `[a-z0-9!#$&\-^_.+]`
### Size Limits
```bash
FLASKPASTE_MAX_ANON=3145728 # 3 MiB for anonymous
FLASKPASTE_MAX_AUTH=52428800 # 50 MiB for authenticated
```
### Password Limits
- Maximum 1024 characters (prevents DoS via hashing)
- Unicode supported
- Special characters allowed
## SQL Injection Prevention
All database queries use parameterized statements:
```python
# Correct - parameterized
db.execute("SELECT * FROM pastes WHERE id = ?", (paste_id,))
# Never - string concatenation
db.execute(f"SELECT * FROM pastes WHERE id = '{paste_id}'")
```
## Database Security
- SQLite with WAL mode for better concurrency
- Foreign keys enforced
- Automatic cleanup of expired content
- No sensitive data in plaintext (passwords are hashed)
## Deployment Recommendations
### Reverse Proxy
Always deploy behind a TLS-terminating reverse proxy:
```
┌─────────┐ TLS ┌─────────┐ HTTP ┌───────────┐
│ Client │ ──────────── │ nginx │ ──────────── │ FlaskPaste│
│ │ mTLS opt │ HAProxy │ local │ │
└─────────┘ └─────────┘ └───────────┘
```
### Container Security
```dockerfile
# Run as non-root
USER app
# Read-only filesystem where possible
# Mount /app/data as writable volume
```
### Network Isolation
- FlaskPaste should only listen on localhost or internal network
- Reverse proxy handles external traffic
- Use firewall rules to restrict direct access
### Secrets Management
```bash
# Use strong, unique secrets
FLASKPASTE_PROXY_SECRET="$(openssl rand -hex 32)"
FLASKPASTE_POW_SECRET="$(openssl rand -hex 32)"
```
## Security Checklist
### Before Deployment
- [ ] Deploy behind TLS-terminating reverse proxy
- [ ] Configure `FLASKPASTE_PROXY_SECRET` if using auth headers
- [ ] Enable proof-of-work if public-facing
- [ ] Set appropriate size limits
- [ ] Configure paste expiry
### Ongoing
- [ ] Monitor logs for suspicious activity
- [ ] Keep dependencies updated
- [ ] Review access patterns
- [ ] Rotate secrets periodically
- [ ] Test backup/restore procedures
## Known Limitations
1. **No rate limiting per IP** - Delegated to reverse proxy
2. **No user accounts** - PKI handles identity
3. **No audit log** - Standard request logging only
4. **Single-node only** - SQLite limits horizontal scaling
## Reporting Vulnerabilities
If you discover a security vulnerability:
1. **Do not** open a public issue
2. Email security concerns to the repository maintainer
3. Include:
- Description of the vulnerability
- Steps to reproduce
- Potential impact
- Suggested fix (if any)
We aim to acknowledge reports within 48 hours and provide a fix timeline within 7 days.
## Security Updates
Security fixes are released as soon as possible. Subscribe to repository releases for notifications.
## Threat Model
### In Scope
- Unauthorized access to pastes
- Content injection attacks
- Authentication bypass
- Information disclosure
- Denial of service (application-level)
### Out of Scope
- Physical access to server
- Compromise of reverse proxy
- Client-side vulnerabilities
- Network-level DoS
- Social engineering
## Version History
| Version | Security Changes |
|---------|------------------|
| 1.2.0 | Password protection with PBKDF2, code modernization |
| 1.1.0 | E2E encryption, entropy enforcement, burn-after-read |
| 1.0.0 | Initial release with core security features |

117
pyproject.toml Normal file
View File

@@ -0,0 +1,117 @@
[project]
name = "flaskpaste"
version = "1.2.0"
description = "Secure pastebin with mTLS authentication and E2E encryption"
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"flask>=3.0",
"cryptography>=42.0",
]
[project.optional-dependencies]
dev = [
"pytest>=8.0",
"pytest-cov>=4.1",
"ruff>=0.8",
"mypy>=1.8",
"bandit>=1.7",
"pip-audit>=2.6",
]
# ─────────────────────────────────────────────────────────────────────────────
# Ruff - Fast Python linter and formatter
# ─────────────────────────────────────────────────────────────────────────────
[tool.ruff]
target-version = "py311"
line-length = 100
exclude = [
".git",
".venv",
"venv",
"__pycache__",
"*.egg-info",
]
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"B", # flake8-bugbear
"S", # flake8-bandit (security)
"UP", # pyupgrade
"SIM", # flake8-simplify
"TCH", # type-checking imports
"RUF", # ruff-specific
]
ignore = [
"S101", # assert allowed in tests
"S311", # pseudo-random ok for non-crypto
"B008", # function call in default arg (Flask patterns)
]
[tool.ruff.lint.per-file-ignores]
"tests/*" = ["S101", "S105", "S106"] # Allow asserts and hardcoded passwords in tests
"fpaste" = ["S603", "S607"] # Subprocess calls ok in CLI
"app/config.py" = ["S105"] # Test config has hardcoded passwords
[tool.ruff.lint.isort]
known-first-party = ["app"]
# ─────────────────────────────────────────────────────────────────────────────
# Mypy - Static type checking
# ─────────────────────────────────────────────────────────────────────────────
[tool.mypy]
python_version = "3.11"
warn_return_any = true
warn_unused_ignores = true
warn_redundant_casts = true
warn_unused_configs = true
show_error_codes = true
pretty = true
# Strict mode components (enable incrementally)
check_untyped_defs = true
disallow_untyped_defs = false # Enable later for stricter checking
disallow_incomplete_defs = true
[[tool.mypy.overrides]]
module = [
"flask.*",
"cryptography.*",
"werkzeug.*",
]
ignore_missing_imports = true
# ─────────────────────────────────────────────────────────────────────────────
# Bandit - Security linter
# ─────────────────────────────────────────────────────────────────────────────
[tool.bandit]
exclude_dirs = ["tests", "venv", ".venv"]
skips = ["B101"] # assert_used - ok in production for invariants
# ─────────────────────────────────────────────────────────────────────────────
# Pytest
# ─────────────────────────────────────────────────────────────────────────────
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = "test_*.py"
python_functions = "test_*"
addopts = "-v --tb=short"
# ─────────────────────────────────────────────────────────────────────────────
# Coverage
# ─────────────────────────────────────────────────────────────────────────────
[tool.coverage.run]
source = ["app"]
branch = true
omit = ["tests/*"]
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"if TYPE_CHECKING:",
"raise NotImplementedError",
]

17
requirements-dev.txt Normal file
View File

@@ -0,0 +1,17 @@
# Development and CI dependencies
# Install: pip install -r requirements-dev.txt
# Testing
pytest>=8.0
pytest-cov>=4.1
# Code quality
ruff>=0.8
mypy>=1.8
# Security analysis
bandit>=1.7
pip-audit>=2.6
# Type stubs
types-requests>=2.31