Security Best Practices
Security standards and practices for FLYPILOT development projects.
Credential Management
1Password
All credentials stored in 1Password vaults:
| Vault | Contents |
|---|---|
| Engineering | API keys, service accounts |
| Client - [Name] | Client-specific credentials |
| Hosting | Server access, hosting panels |
Environment Variables
Never commit credentials to Git.
# .env.local (gitignored)
DATABASE_URL=postgresql://user:pass@host:5432/db
API_KEY=sk_live_xxxxxxxxxxxx
1Password CLI
# Inject secrets at runtime
op run --env-file=.env -- npm run start
# Reference in .env
DATABASE_URL=op://Engineering/Database/connection-string
Code Security
Input Validation
Always validate and sanitize user input:
// Good: Validate input
import { z } from 'zod';
const userSchema = z.object({
email: z.string().email(),
name: z.string().min(1).max(100),
age: z.number().int().positive().optional(),
});
function createUser(data: unknown) {
const validated = userSchema.parse(data);
// Use validated data
}
// WordPress: Sanitize input
$email = sanitize_email($_POST['email']);
$title = sanitize_text_field($_POST['title']);
$content = wp_kses_post($_POST['content']);
Output Encoding
Always encode output to prevent XSS:
// React automatically escapes (safe)
<div>{userInput}</div>
// Dangerous: Only use with sanitized content
<div dangerouslySetInnerHTML={{ __html: sanitizedHtml }} />
// WordPress: Escape output
echo esc_html($title);
echo esc_url($link);
echo esc_attr($attribute);
echo wp_kses_post($content);
SQL Injection Prevention
Always use parameterized queries:
// Good: Parameterized query
const user = await db.query(
'SELECT * FROM users WHERE id = $1',
[userId]
);
// Bad: String concatenation
const user = await db.query(
`SELECT * FROM users WHERE id = ${userId}` // VULNERABLE!
);
// WordPress: Use $wpdb->prepare()
$results = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$wpdb->posts} WHERE post_author = %d",
$author_id
)
);
Authentication
Password Requirements
- Minimum 12 characters
- Mix of uppercase, lowercase, numbers
- No common passwords
- Unique per service
Session Security
// Next.js with next-auth
export const authOptions: NextAuthOptions = {
session: {
strategy: 'jwt',
maxAge: 30 * 24 * 60 * 60, // 30 days
},
cookies: {
sessionToken: {
name: '__Secure-next-auth.session-token',
options: {
httpOnly: true,
sameSite: 'lax',
path: '/',
secure: true,
},
},
},
};
API Authentication
// Verify API key
function authenticateRequest(req: Request) {
const apiKey = req.headers.get('x-api-key');
if (!apiKey) {
throw new Error('API key required');
}
// Use timing-safe comparison
const valid = crypto.timingSafeEqual(
Buffer.from(apiKey),
Buffer.from(process.env.API_KEY!)
);
if (!valid) {
throw new Error('Invalid API key');
}
}
HTTPS & Headers
Required Security Headers
// next.config.js
const securityHeaders = [
{
key: 'X-DNS-Prefetch-Control',
value: 'on',
},
{
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains; preload',
},
{
key: 'X-Frame-Options',
value: 'SAMEORIGIN',
},
{
key: 'X-Content-Type-Options',
value: 'nosniff',
},
{
key: 'Referrer-Policy',
value: 'origin-when-cross-origin',
},
{
key: 'Permissions-Policy',
value: 'camera=(), microphone=(), geolocation=()',
},
];
module.exports = {
async headers() {
return [
{
source: '/:path*',
headers: securityHeaders,
},
];
},
};
Content Security Policy
const ContentSecurityPolicy = `
default-src 'self';
script-src 'self' 'unsafe-eval' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self' blob: data: https:;
font-src 'self';
connect-src 'self' https://api.example.com;
frame-ancestors 'none';
`;
WordPress Security
Plugin Security
// Verify nonces in forms
wp_nonce_field('my_action', 'my_nonce');
// Verify nonce on submission
if (!wp_verify_nonce($_POST['my_nonce'], 'my_action')) {
wp_die('Security check failed');
}
// Check user capabilities
if (!current_user_can('edit_posts')) {
wp_die('Unauthorized');
}
File Upload Security
// Restrict upload types
function restrict_upload_types($mimes) {
return [
'jpg|jpeg|jpe' => 'image/jpeg',
'gif' => 'image/gif',
'png' => 'image/png',
'pdf' => 'application/pdf',
];
}
add_filter('upload_mimes', 'restrict_upload_types');
Disable XML-RPC
// Disable XML-RPC if not needed
add_filter('xmlrpc_enabled', '__return_false');
Dependency Security
Audit Dependencies
# Node.js
npm audit
npm audit fix
# WordPress
wp plugin update --all
wp core update
Automated Security Scanning
# .github/workflows/security.yml
name: Security Scan
on:
push:
branches: [main]
schedule:
- cron: '0 0 * * 1' # Weekly
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run npm audit
run: npm audit --audit-level=high
- name: Run Snyk scan
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
Incident Response
If Credentials Exposed
- Immediately rotate the exposed credential
- Audit access logs for unauthorized use
- Notify stakeholders if data accessed
- Document incident for post-mortem
If Site Compromised
- Take site offline if necessary
- Preserve evidence (logs, files)
- Identify entry point
- Clean and restore from backup
- Patch vulnerability
- Document and report
Security Checklist
Pre-Launch
- HTTPS configured
- Security headers set
- Input validation implemented
- Output encoding in place
- SQL injection prevented
- Authentication secure
- Dependencies audited
- Error messages sanitized
- Logging configured
- Backups working
Ongoing
- Weekly dependency audits
- Monthly security review
- Quarterly penetration test
- Annual security training
Reporting Security Issues
Report security vulnerabilities immediately to the tech lead. Do not discuss publicly until resolved.