Grav Security — Hardening, CSP, XSS Prevention and Secure Configuration
In this tutorial, you'll learn Grav security — hardening your Grav installation, configuring .htaccess and security headers, preventing XSS Attacks, setting file permissions, and following secure deployment practices.
What You'll Learn
- Grav-specific security considerations (flat-file CMS)
- Server hardening: .htaccess, Nginx rules, file permissions
- Content Security Policy (CSP) headers
- XSS prevention in Twig templates
- Secure configuration: admin routes, user accounts, plugins
- Monitoring and logging security events
Why It Matters
In WordPress, security is a constant concern with plugin vulnerabilities, database injection, and brute force attacks. In Grav, the flat-file architecture eliminates database injection entirely. But Grav still needs proper security — file permissions, secure headers, template escaping, and admin protection. A secure Grav site is the result of conscious configuration, not default settings.
Real-World Use
A financial services company uses Grav for their internal documentation portal. Security requirements include: all traffic over HTTPS, strict CSP headers preventing XSS, admin panel accessible only from the office VPN, automated security scanning on every deployment, and comprehensive access logging. The flat-file architecture passes the security audit because there is no database to inject, and all user input is properly escaped by Twig.
Learning Path
flowchart LR
A["Performance Optimization"] --> B["Security
← You are here"]:::current
B --> C["Git Workflow"]
C --> D["CLI Tools"]
D --> E["Production Deployment"]
classDef current fill:#38bdf8,color:#0f172a,stroke-width:2px
File Permissions
Set restrictive file permissions for production:
# Directories: 755 (rwxr-xr-x)
find . -type d -exec chmod 755 {} \;
# Files: 644 (rw-r--r--)
find . -type f -exec chmod 644 {} \;
# Sensitive files: 640 (rw-r-----)
chmod 640 user/config/system.yaml
chmod 640 user/config/site.yaml
chmod 640 .grav/config/*.yaml
# Writable directories (needed for uploads and cache)
chmod 775 user/data
chmod 775 user/data/cache
chmod 775 user/data/images
chmod 775 user/data/uploads
chmod 775 user/accounts
chmod 775 user/config
Security Headers
Apache (.htaccess)
Add to user/.htaccess:
# Security headers
<IfModule mod_headers.c>
# HTTPS enforced
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
# Content Security Policy
Header always set Content-Security-Policy "
default-src 'self';
script-src 'self' 'unsafe-inline' https://www.google-analytics.com;
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
img-src 'self' data: https:;
font-src 'self' https://fonts.gstatic.com;
frame-src 'none';
object-src 'none';
base-uri 'self';
form-action 'self';
"
# XSS protection
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "DENY"
Header always set X-XSS-Protection "1; mode=block"
# Referrer policy
Header always set Referrer-Policy "strict-origin-when-cross-origin"
# Permissions policy
Header always set Permissions-Policy "
geolocation=(),
microphone=(),
camera=(),
payment=()
"
</IfModule>
# Deny access to sensitive files
<FilesMatch "\.(yaml|md|twig|php~|git|log)$">
Require all denied
</FilesMatch>
# Deny access to dot files
<FilesMatch "^\.">
Require all denied
</FilesMatch>
# Disable directory listing
Options -Indexes
Nginx
Add to server block:
# Security headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" 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'; img-src 'self' data: https:; font-src 'self'; frame-src 'none'; object-src 'none'; form-action 'self';" always;
# Deny access to sensitive files
location ~* \.(yaml|md|twig|log|git)$ {
deny all;
return 404;
}
location ~ /\. {
deny all;
return 404;
}
# Deny access to user data
location ~ ^/user/data/(cache|images|accounts) {
deny all;
return 404;
}
XSS Prevention in Twig
Twig automatically escapes output, but there are important considerations:
{# Safe - autoescaped as HTML #}
{{ page.title }}
{# Safe - autoescaped as HTML attribute #}
<a href="{{ url }}">{{ label }}</a>
{# POTENTIAL XSS - use raw only on trusted content #}
{{ page.content|raw }}
{# Safe - explicitly escaped for JS context #}
<script>
var data = {{ user_data|json_encode|raw }};
</script>
{# Use the e filter for specific escaping strategies #}
{{ user_input|e('html') }} {# HTML escape (default) #}
{{ user_input|e('js') }} {# JavaScript escape #}
{{ user_input|e('css') }} {# CSS escape #}
{{ user_input|e('url') }} {# URL escape #}
Never Use Raw On:
{# DANGEROUS - user input should never use raw #}
{{ user_provided_name|raw }}
{# DANGEROUS - form data should not be output raw #}
{{ form.value.message|raw }}
Admin Panel Security
Change Admin Route
# user/config/plugins/admin.yaml
route: '/secure-admin-panel'
Two-Factor Authentication
# user/config/plugins/admin.yaml
twofa_enabled: true
IP Whitelist
# .htaccess - restrict admin to office IPs
<Location "/secure-admin-panel">
Require ip 192.168.1.0/24
Require ip 10.0.0.0/8
</Location>
Session Security
# user/config/system.yaml
session:
enabled: true
timeout: 1800 # 30 minutes
secure: true # HTTPS only
httponly: true # Not accessible via JS
Grav-Specific Security
Disable Guest Accounts
# user/config/plugins/login.yaml
dynamic_site_creation: false
Disable Twig Auto-Reload in Production
# user/config/system.yaml
twig:
auto_reload: false
cache: true
Disable Debug Mode
# user/config/system.yaml
debugger:
enabled: false
twig: false
Security Monitoring
// user/plugins/security-logger/security-logger.php
public function onAdminLogin($event)
{
$user = $event['user'];
$success = $event['success'];
$ip = $_SERVER['REMOTE_ADDR'];
$logEntry = [
'time' => time(),
'username' => $user->username,
'success' => $success,
'ip' => $ip,
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown',
];
file_put_contents(
GRAV_ROOT . '/user/data/logs/auth.log',
json_encode($logEntry) . "\n",
FILE_APPEND
);
// Alert on repeated failures
if (!$success) {
$this->checkBruteForce($ip);
}
}
Learning Path
flowchart LR
A["Performance Optimization"] --> B["Security
← You are here"]:::current
B --> C["Git Workflow"]
C --> D["CLI Tools"]
D --> E["Production Deployment"]
classDef current fill:#38bdf8,color:#0f172a,stroke-width:2px
Common Mistakes
Using
|rawon user-generated content: Twig autoescapes by default for a reason. Using|rawon form input, comments, or any user-provided data opens the site to XSS attacks.Not restricting admin access: Leaving the admin panel at
/adminwith no IP restriction or 2FA makes brute force attacks possible. Change the route and enable 2FA.Wrong file permissions: Setting files or directories to 777 allows any user on the server to modify them. Use 755 for directories, 644 for files, and 640 for sensitive config files.
Leaving debug mode enabled in production: The debug bar exposes configuration values, user accounts, and template structure. Always disable the debugger in production.
Not setting HTTPS headers: Without Strict-Transport-Security, browsers may connect over HTTP. Without X-Frame-Options, your site can be embedded in iframes (clickjacking). Always set security headers.
Practice Questions
Why is Grav inherently more secure against SQL Injection than WordPress? Answer: Grav has no database. All content is stored in files. SQL injection attacks, which target database queries, are impossible because there are no queries to inject.
What are the minimum file permissions for a Grav production site? Answer: 755 for directories, 644 for regular files, 640 for config files (system.yaml, site.yaml), and 775 for writable directories (user/data, user/accounts, user/config).
How does Twig prevent XSS attacks by default? Answer: Twig autoescapes all output as HTML unless explicitly marked with
|raw. This converts characters like<,>,",&into safe HTML entities, preventing script injection.What security headers should every Grav site include? Answer: Content-Security-Policy, X-Content-Type-Options (nosniff), X-Frame-Options (DENY), Strict-Transport-Security (HSTS), Referrer-Policy, and Permissions-Policy.
Challenge: Perform a complete security audit of a Grav installation. Check: file permissions (all directories and files), security headers (use securityheaders.com), admin panel security (route, 2FA, IP restriction), Twig templates (no dangerous
|rawusage), user accounts (no default passwords, proper access levels), debug mode (disabled in production), session configuration (secure, httponly), and CSP headers (properly configured). Fix all identified issues and document each change.
FAQ
Mini Project
Goal: Harden a Grav installation against common security threats.
- Set correct file permissions across the entire installation
- Configure security headers (.htaccess for Apache, add_header for Nginx)
- Create a strict Content Security Policy
- Change the admin route and enable 2FA
- Disable debug mode and Twig auto-reload
- Add brute force protection logging
- Set secure session configuration
- Review all templates for unsafe
|rawusage - Run a security audit with tools like OWASP ZAP
- Create a security checklist document for future deployments
What's Next
Now your site is secure. Next, learn Git-based workflows:
Continue to Lesson 38: Git Workflow — Git-based deployment, CI/CD, and multi-environment setup.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro