Skip to content

Grav Security — Hardening, CSP, XSS Prevention and Secure Configuration

DodaTech Updated 2026-06-27 7 min read

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

  1. Using |raw on user-generated content: Twig autoescapes by default for a reason. Using |raw on form input, comments, or any user-provided data opens the site to XSS attacks.

  2. Not restricting admin access: Leaving the admin panel at /admin with no IP restriction or 2FA makes brute force attacks possible. Change the route and enable 2FA.

  3. 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.

  4. Leaving debug mode enabled in production: The debug bar exposes configuration values, user accounts, and template structure. Always disable the debugger in production.

  5. 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

  1. 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.

  2. 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).

  3. 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.

  4. 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.

  5. 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 |raw usage), 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

Can Grav be hacked if there is no database?

Grav is generally more secure than database-driven CMSs because SQL injection is impossible. However, vulnerabilities can still exist in plugins, themes, server configuration, and file permissions.

How do I protect the admin panel from brute force attacks?

Change the admin route from /admin to a custom path, enable 2FA, restrict admin access by IP, use strong passwords, and implement login rate limiting.

What is a Content Security Policy and why do I need it?

CSP is an HTTP header that tells the browser which sources of content are trusted. It prevents XSS attacks by blocking inline scripts and unauthorized resource loading.

How often should I update Grav and its plugins?

Check for updates weekly. Security patches are released as needed. Subscribe to Grav's security advisory mailing list for critical updates.

Is it safe to use `|raw` on content from the Admin panel?

Content from trusted administrators is generally safe, but it is best practice to avoid |raw wherever possible. If you must use it, ensure the content is sanitized first.

Mini Project

Goal: Harden a Grav installation against common security threats.

  1. Set correct file permissions across the entire installation
  2. Configure security headers (.htaccess for Apache, add_header for Nginx)
  3. Create a strict Content Security Policy
  4. Change the admin route and enable 2FA
  5. Disable debug mode and Twig auto-reload
  6. Add brute force protection logging
  7. Set secure session configuration
  8. Review all templates for unsafe |raw usage
  9. Run a security audit with tools like OWASP ZAP
  10. 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