forked from claw/flaskpaste
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:
85
Makefile
Normal file
85
Makefile
Normal 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
265
SECURITY.md
Normal 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
117
pyproject.toml
Normal 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
17
requirements-dev.txt
Normal 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
|
||||
Reference in New Issue
Block a user