Security
Security best practices and hardening guide for Duckling.
Security Audit Summary
Last audit: December 2025
Vulnerability Status
| Category | Status | Notes |
|---|---|---|
| Dependency vulnerabilities | ✅ Fixed | Updated flask-cors, gunicorn, werkzeug |
| Flask debug mode | ✅ Fixed | Now uses environment variables |
| Path traversal | ✅ Fixed | Added path validation |
| SQL injection | ✅ Protected | Using SQLAlchemy ORM with parameterized queries |
| XSS (Cross-Site Scripting) | ⚠️ Mitigated | Uses dangerouslySetInnerHTML for trusted docs only |
| CORS | ✅ Configured | Restricted to localhost origins in development |
Production Deployment Checklist
Before deploying to production, ensure:
- Set
FLASK_DEBUG=falseenvironment variable - Set a strong
SECRET_KEYenvironment variable - Configure
FLASK_HOSTappropriately (not 0.0.0.0 unless behind reverse proxy) - Update CORS origins in
backend/duckling.pyto match your domain - Use HTTPS in production (configure via reverse proxy)
- Set appropriate
MAX_CONTENT_LENGTHfor your use case - Review and restrict file upload extensions if needed
- Set
DUCKLING_API_KEY(orapi_keyin server config file) and terminate TLS at your reverse proxy - Set
DUCKLING_LOG_FORMAT=jsonfor structured production logs - Configure
DUCKLING_CONFIG_FILEfor centralized deploy settings
API key authentication
When DUCKLING_API_KEY is set, Duckling requires the header on all /api/* requests except health and embedded docs:
Configure via environment variable or api_key in Server Configuration. The frontend build can set VITE_API_KEY so the UI sends the header automatically.
401 Unauthorized is returned when the key is missing or wrong.
Defense in depth
Even with API key auth, place Duckling behind HTTPS and restrict network access. The API key protects programmatic access; it does not replace network-level controls.
| Variable | Default | Description |
|---|---|---|
FLASK_DEBUG | false | Enable debug mode (never in production) |
FLASK_HOST | 127.0.0.1 | Host to bind to |
FLASK_PORT | 5001 | Port to listen on |
SECRET_KEY | dev-secret-key... | Flask secret key (MUST change in production) |
MAX_CONTENT_LENGTH | 104857600 | Max upload size in bytes (100MB) |
Secret Key
Generate a secure secret key for production:
Security Measures
Container Runtime Hardening
Duckling production compose files support hardened container defaults:
- run containers with a read-only root filesystem
- drop all Linux capabilities (
cap_drop: ["ALL"]) - enforce
no-new-privileges - use explicit writable
tmpfsmounts for transient runtime paths - run backend/frontend as non-root users inside the image
For production-like runs, use:
For pre-built images, docker-compose.prebuilt.yml includes the same hardening defaults.
Container Supply Chain Hardening
The Docker image publish workflow includes:
- vulnerability scanning gate (Trivy, fails on
HIGHandCRITICAL; installstrivyon the runner,docker pulls the published tags, then runstrivy imageso registry authentication matches the workflow logins) - SBOM generation (Syft SPDX JSON artifacts)
- build provenance enabled for
buildxpublishes - keyless image signing (Cosign)
These controls are defined in .github/workflows/publish-docker.yml.
Backend Security
1. Environment-Based Configuration
- Debug mode disabled by default
- Secret keys loaded from environment variables
- Host binding defaults to localhost (127.0.0.1)
2. Input Validation
- File upload validation (extension whitelist)
- File size limits (100MB default)
- Search query length limits and sanitization
3. Path Traversal Protection
- All file serving endpoints validate paths
- Resolved paths checked against allowed directories
- Directory traversal sequences blocked
def validate_path(path: str, allowed_dir: str) -> bool:
"""Ensure path doesn't escape allowed directory."""
resolved = os.path.realpath(path)
return resolved.startswith(os.path.realpath(allowed_dir))
4. Database Security
- SQLAlchemy ORM prevents SQL injection
- Parameterized queries for all database operations
- LIKE wildcards escaped in search queries
5. CORS Configuration
- Origins restricted to localhost in development
- Configurable for production deployments
Frontend Security
1. Content Security
- Documentation rendering uses trusted backend-generated HTML
- No user-generated content rendered as HTML
2. API Communication
- All API calls use typed interfaces
- Error responses handled gracefully
HTTPS Configuration
Let's Encrypt with Certbot
# Install certbot
sudo apt install certbot python3-certbot-nginx
# Obtain certificate
sudo certbot --nginx -d docling.example.com
# Auto-renewal (usually configured automatically)
sudo certbot renew --dry-run
Nginx SSL Configuration
server {
listen 443 ssl http2;
server_name docling.example.com;
ssl_certificate /etc/letsencrypt/live/docling.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/docling.example.com/privkey.pem;
# Modern SSL configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers off;
# HSTS
add_header Strict-Transport-Security "max-age=63072000" always;
}
Rate Limiting
Nginx Rate Limiting
# Define rate limit zone
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
server {
location /api/ {
limit_req zone=api burst=20 nodelay;
proxy_pass http://localhost:5001;
}
}
Flask-Limiter
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/convert", methods=["POST"])
@limiter.limit("10 per minute")
def convert():
pass
File Upload Security
Allowed Extensions
ALLOWED_EXTENSIONS = {
'pdf', 'docx', 'pptx', 'xlsx',
'html', 'htm', 'md', 'markdown',
'png', 'jpg', 'jpeg', 'tiff', 'gif', 'webp', 'bmp',
'asciidoc', 'adoc', 'xml'
}
def allowed_file(filename: str) -> bool:
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
Filename Sanitization
from werkzeug.utils import secure_filename
def sanitize_filename(filename: str) -> str:
"""Sanitize filename for safe storage."""
return secure_filename(filename)
Security Headers
Nginx Headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';" always;
Flask Headers
@app.after_request
def add_security_headers(response):
response.headers['X-Frame-Options'] = 'SAMEORIGIN'
response.headers['X-Content-Type-Options'] = 'nosniff'
response.headers['X-XSS-Protection'] = '1; mode=block'
return response
Dependency Security
Python Dependencies
When Docker publish workflows enforce Trivy scan gates, keep packaging-related Python dependencies in backend/requirements.txt at secure minimum versions (including jaraco.context and wheel) to avoid known high-severity CVEs during image scans. Keep Ray out of the default backend image requirements (use optional backend/requirements-orchestration.txt) because Ray ships Java JARs that can fail Trivy until upstream patches land.
For GitHub Dependabot/npm audit findings, bump direct frontend/backend dependencies and regenerate frontend/package-lock.json; use npm overrides for transitive packages when upstream pins lag (for example minimatch, js-yaml, ws, nanoid, form-data).
Node.js Dependencies
Reporting Vulnerabilities
If you discover a security vulnerability:
- Do NOT open a public issue
- Email the maintainers directly with:
- Description of the vulnerability
- Steps to reproduce
- Potential impact
- Suggested fix (if any)
We will respond within 48 hours and work with you to:
- Confirm the vulnerability
- Develop a fix
- Coordinate disclosure
Known Limitations
- XSS in Documentation Viewer: The docs panel uses
dangerouslySetInnerHTMLto render markdown-converted HTML. This is acceptable because: - Documentation is served from local files only
- No user-generated content is rendered
-
Content is converted server-side with trusted markdown library
-
Local File Access: The application reads and writes files to configured directories. Ensure proper filesystem permissions.
-
No Authentication: This application is designed for local/personal use and does not include user authentication. For multi-user deployments, add authentication via a reverse proxy or middleware.