Skip to content

Security

When building WordPress themes with Foehn, security should be a top priority. This guide covers essential security practices, focusing on output escaping to prevent Cross-Site Scripting (XSS) vulnerabilities.

Output Escaping

Never trust user input. All dynamic content must be escaped before output, using the appropriate function for the context.

Escaping Functions

FunctionUse CaseExample
esc_html()Text content inside HTML elements<p><?php echo esc_html($text); ?></p>
esc_attr()HTML attribute values<div class="<?php echo esc_attr($class); ?>">
esc_url()URLs (href, src, etc.)<a href="<?php echo esc_url($url); ?>">
esc_textarea()Textarea content<textarea><?php echo esc_textarea($text); ?></textarea>
esc_js()Inline JavaScript strings<script>var x = '<?php echo esc_js($val); ?>';</script>
wp_kses_post()HTML content (allows safe tags)<div><?php echo wp_kses_post($html); ?></div>
wp_kses()HTML with custom allowed tagsecho wp_kses($html, $allowed_tags);

When to Use Each Function

esc_html() — Plain Text

Use for any text that should not contain HTML:

php
// ✅ Good: Escaped output
echo '<h1>' . esc_html($title) . '</h1>';

// ❌ Bad: Unescaped user input
echo '<h1>' . $title . '</h1>';

esc_attr() — HTML Attributes

Use for values inside HTML attributes:

php
// ✅ Good: Escaped attribute
echo '<input type="text" value="' . esc_attr($value) . '">';
echo '<div class="' . esc_attr($classes) . '">';
echo '<div data-config="' . esc_attr(json_encode($config)) . '">';

// ❌ Bad: Unescaped attribute (XSS via " onclick="alert(1))
echo '<input type="text" value="' . $value . '">';

esc_url() — URLs

Use for any URL output:

php
// ✅ Good: Escaped URL
echo '<a href="' . esc_url($link) . '">Click</a>';
echo '<img src="' . esc_url($image_url) . '">';

// ❌ Bad: Allows javascript: protocol XSS
echo '<a href="' . $link . '">Click</a>';

wp_kses_post() — Rich HTML Content

Use when you need to allow safe HTML (like post content):

php
// ✅ Good: Allows safe HTML, strips dangerous tags
echo '<div class="content">' . wp_kses_post($content) . '</div>';

// ❌ Bad: Allows all HTML including <script>
echo '<div class="content">' . $content . '</div>';

wp_kses() — Custom Allowed Tags

Use when you need fine-grained control over allowed HTML:

php
$allowed = [
    'a' => ['href' => [], 'title' => [], 'target' => []],
    'strong' => [],
    'em' => [],
];

echo wp_kses($user_bio, $allowed);

Shortcode Security

Shortcodes are particularly vulnerable because they process user-provided attributes and content. Always escape output:

php
#[AsShortcode('greeting')]
public function greeting(array $atts, ?string $content = null): string
{
    $atts = shortcode_atts([
        'name' => 'World',
        'class' => 'greeting',
    ], $atts);

    // ✅ Every dynamic value is escaped appropriately
    return sprintf(
        '<div class="%s"><p>Hello, %s!</p>%s</div>',
        esc_attr($atts['class']),      // Attribute context
        esc_html($atts['name']),       // Text context
        wp_kses_post($content)         // HTML content context
    );
}

Common Shortcode Mistakes

php
// ❌ DANGEROUS: No escaping
#[AsShortcode('unsafe')]
public function unsafe(array $atts): string
{
    return '<div class="' . $atts['class'] . '">' . $atts['content'] . '</div>';
}

// ✅ SAFE: Properly escaped
#[AsShortcode('safe')]
public function safe(array $atts): string
{
    $atts = shortcode_atts([
        'class' => 'default',
        'content' => '',
    ], $atts);

    return sprintf(
        '<div class="%s">%s</div>',
        esc_attr($atts['class']),
        esc_html($atts['content'])
    );
}

See Shortcodes Guide for more examples.

Twig Template Security

Timber/Twig auto-escapes output by default, but be aware of the |raw filter:

twig
{# ✅ Safe: Auto-escaped by Twig #}
<h1>{{ post.title }}</h1>
<p>{{ user_input }}</p>

{# ⚠️ Careful: |raw disables escaping - only use for trusted HTML #}
<div>{{ post.content|raw }}</div>

{# ✅ Safe: Use e() filter with context for explicit escaping #}
<a href="{{ url|e('url') }}">{{ text|e('html') }}</a>
<div data-value="{{ value|e('html_attr') }}">

When to Use |raw

Only use |raw for content that:

  1. Comes from WordPress's post content (already sanitized on save)
  2. Is generated by trusted code (not user input)
  3. Has been explicitly sanitized with wp_kses_post() or similar
twig
{# ✅ OK: Post content from WordPress #}
{{ post.content|raw }}

{# ✅ OK: Rendered shortcode output #}
{{ shortcode_output|raw }}

{# ❌ NEVER: Direct user input #}
{{ request.get('comment')|raw }}

REST API Security

When building REST endpoints, validate and sanitize all input:

php
#[AsRestRoute('/contact', methods: ['POST'])]
public function handleContact(\WP_REST_Request $request): \WP_REST_Response
{
    // ✅ Validate required fields
    $email = sanitize_email($request->get_param('email'));
    $message = sanitize_textarea_field($request->get_param('message'));

    if (!is_email($email)) {
        return new \WP_REST_Response(['error' => 'Invalid email'], 400);
    }

    // Process the sanitized data...
}

Security Checklist

Before deploying, verify:

  • [ ] All shortcode output uses appropriate escaping functions
  • [ ] Twig templates don't use |raw on untrusted content
  • [ ] REST API endpoints validate and sanitize input
  • [ ] Database queries use prepared statements ($wpdb->prepare())
  • [ ] Nonces are verified for form submissions
  • [ ] User capabilities are checked before privileged operations
  • [ ] File uploads are validated (type, size, content)

Additional Resources

Released under the MIT License.