This document outlines security considerations and best practices for deploying and operating MarkGo.
IMPORTANT: The .env file is automatically excluded from version control via .gitignore. However, you must ensure:
-
Never commit
.envto Git# Verify .env is ignored git check-ignore .env # Should output: .env
-
Use
.env.exampleas a template- Copy
.env.exampleto.envfor local development .env.examplecontains no secrets, only placeholders- Safe to commit
.env.exampleto show required configuration
- Copy
-
Rotate secrets if accidentally committed
- If you accidentally commit secrets to Git:
- Immediately rotate all exposed credentials (SMTP passwords, API keys, etc.)
- Remove the commit from Git history (use
git filter-branchor BFG Repo-Cleaner) - Never assume deletion removes the secret - anyone who pulled the repo has it
- If you accidentally commit secrets to Git:
# SMTP Credentials (for contact form)
EMAIL_SMTP_PASSWORD="your-smtp-password-here" # Keep secret!
# Admin Credentials (for /admin and /debug endpoints)
ADMIN_USERNAME="admin" # Change from default
ADMIN_PASSWORD="secure-password" # Use strong password (16+ chars, mixed case, numbers, symbols)-
.envfile is NOT in version control - SMTP password is stored securely (use secrets manager in production)
- Admin password is strong and unique
- Admin credentials are different from default values
- Environment-specific
.envfiles are used (dev, staging, prod)
MarkGo uses session-based authentication for admin, compose, and draft endpoints:
- Login form at
/loginwith session cookie HttpOnly,SameSite=Strictcookie attributes- Sessions expire after inactivity
Recommendations:
-
Always use HTTPS in production
# Example nginx config server { listen 443 ssl; ssl_certificate /path/to/cert.pem; ssl_certificate_key /path/to/key.pem; location / { proxy_pass http://localhost:3000; } }
-
Restrict admin access by IP (recommended for production)
location /admin { allow 192.168.1.0/24; # Your office network allow 10.0.0.1; # Your home IP deny all; proxy_pass http://localhost:3000; } location /debug { allow 127.0.0.1; # Localhost only deny all; proxy_pass http://localhost:3000; }
-
Disable debug endpoints in production
- Debug endpoints (
/debug/*) are only enabled whenENVIRONMENT=development - Production deployments automatically disable these endpoints
- If you need debugging in production, use production-grade APM tools instead
- Debug endpoints (
| Endpoint Pattern | Auth Required | Environment | Purpose |
|---|---|---|---|
/admin/* |
β Session | All | Admin dashboard and management |
/compose/* |
β Session + CSRF | All | Content creation and editing |
/debug/* |
β Session | Development only | Runtime profiling and debugging |
/api/* |
β None | All | Public API endpoints |
/writing/* |
β None | All | Public content |
What we fixed: Prevented localhost bypass vulnerability where localhost.evil.com could bypass CORS restrictions.
Implementation: Exact origin matching using map lookup:
// Secure: Only explicitly allowed origins
allowedMap := make(map[string]bool)
for _, origin := range allowedOrigins {
allowedMap[origin] = true
}
// Exact match required
if origin != "" && allowedMap[origin] {
c.Header("Access-Control-Allow-Origin", origin)
}Configuration:
# In .env - specify exact allowed origins
CORS_ALLOWED_ORIGINS="https://yourdomain.com,https://api.yourdomain.com"Protection against:
- Brute force attacks on contact form
- API abuse and spam
- Memory exhaustion attacks
Implementation:
- IP-based rate limiting with 10,000 IP cap (prevents unbounded memory growth)
- Uses
RemoteAddr(notX-Forwarded-For) to prevent header spoofing - Background cleanup every 1 minute removes stale entries
- Separate limits for general requests and contact form
Configuration:
# General rate limit
RATE_LIMIT_GENERAL_REQUESTS=100 # requests per window
RATE_LIMIT_GENERAL_WINDOW=1m # 1 minute window
# Contact form rate limit (stricter)
RATE_LIMIT_CONTACT_REQUESTS=3 # requests per window
RATE_LIMIT_CONTACT_WINDOW=15m # 15 minute windowNote: Math-based CAPTCHA is implemented on contact form for additional protection.
Automatically applied to all responses:
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
Referrer-Policy: strict-origin-when-cross-origin
- Email addresses validated using RFC-compliant regex
- Markdown content sanitized with bluemonday
- File paths validated to prevent directory traversal
- All user input validated before processing
Double-submit cookie pattern on all compose routes:
SameSite=Strict,HttpOnlycookie + hidden form field- Constant-time comparison prevents timing attacks
- Token generation failure aborts with 500 (prevents empty-token bypass)
Secureflag conditional on environment (HTTPS in production, HTTP in development)
Cookie-based sessions replaced Basic Auth for admin, compose, and draft routes:
- Login form at
/loginwith POST action - Session cookie with
HttpOnly,SameSite=Strict - Middleware-enforced on all authenticated routes
Regex ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$ with length limits:
- Prevents CRLF injection in redirects
- Prevents filesystem traversal via URL params
- Content type detection via
http.DetectContentType(not file extension) - Atomic file writes (temp + rename) prevent partial content on disk
Risk Level: Low
Mitigation: Rate limiting on login endpoint prevents brute force. Deploy behind reverse proxy with IP whitelisting for additional protection.
Risk Level: Low
Why: Target audience is single-author blogs. Credentials in .env.
Mitigation: Use strong passwords. Consider oauth2-proxy for multi-user setups.
- Change default admin credentials
- Use strong passwords (16+ characters, mixed case, numbers, symbols)
- Review and set
CORS_ALLOWED_ORIGINSto exact allowed domains - Configure rate limits appropriate for your traffic
- Ensure
.envis in.gitignoreand not committed - Set
ENVIRONMENT=productionto disable debug endpoints
- Deploy behind reverse proxy (nginx, Caddy, etc.)
- Enable HTTPS/TLS with valid certificates
- Configure IP whitelisting for
/adminendpoints - Set up firewall rules to restrict admin access
- Use secrets manager for sensitive config (AWS Secrets Manager, HashiCorp Vault, etc.)
- Test that
/debugendpoints are disabled (should return 404) - Verify CORS headers are correctly applied
- Test rate limiting with multiple requests
- Confirm admin login works with strong password
- Monitor logs for suspicious activity
- Set up automated security scanning (Dependabot, Snyk, etc.)
- Enable structured logging in production
- Monitor failed admin login attempts
- Track rate limit violations
- Alert on unusual traffic patterns
- Regular dependency updates for security patches
# Check for known vulnerabilities
go list -json -deps ./... | nancy sleuth
# Update dependencies
go get -u ./...
go mod tidy# Run gosec security scanner
gosec ./...
# Run staticcheck
staticcheck ./...# Run all tests including security-related tests
go test ./... -v
# Run specific security tests
go test ./internal/middleware -v # CORS and rate limiting
go test ./internal/handlers -v # Contact form validationIf you discover a security vulnerability in MarkGo:
- DO NOT open a public GitHub issue
- Email security concerns to the maintainer privately
- Include:
- Description of the vulnerability
- Steps to reproduce
- Potential impact assessment
- Suggested fix (if available)
We take security seriously and will respond to legitimate reports promptly.
Last Updated: 2026-02-12 Version: 3.1.0