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
| Function | Use Case | Example |
|---|---|---|
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 tags | echo wp_kses($html, $allowed_tags); |
When to Use Each Function
esc_html() — Plain Text
Use for any text that should not contain HTML:
// ✅ 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:
// ✅ 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:
// ✅ 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):
// ✅ 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:
$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:
#[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
// ❌ 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:
{# ✅ 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:
- Comes from WordPress's post content (already sanitized on save)
- Is generated by trusted code (not user input)
- Has been explicitly sanitized with
wp_kses_post()or similar
{# ✅ 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:
#[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
|rawon 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)