Sanitised inline SVG icon helper
Load SVG icons from the theme folder, strip anything unsafe with wp_kses and a strict allow-list, and print them inline.
WordPressUtilitiessvgiconsksessecurity
Code
<?php
/**
* Inline SVG icons from /assets/icons/<name>.svg
*
* @package Acme_Theme
*/
defined( 'ABSPATH' ) || exit;
/** Tags and attributes a plain icon needs. Anything else is removed. */
function acme_svg_allowed_html(): array {
$common = array( 'class' => true, 'fill' => true, 'stroke' => true, 'stroke-width' => true, 'stroke-linecap' => true, 'stroke-linejoin' => true, 'transform' => true, 'opacity' => true );
return array(
'svg' => $common + array( 'xmlns' => true, 'viewbox' => true, 'width' => true, 'height' => true, 'aria-hidden' => true, 'role' => true, 'focusable' => true ),
'g' => $common,
'title' => array(),
'path' => $common + array( 'd' => true, 'fill-rule' => true, 'clip-rule' => true ),
'circle' => $common + array( 'cx' => true, 'cy' => true, 'r' => true ),
'rect' => $common + array( 'x' => true, 'y' => true, 'width' => true, 'height' => true, 'rx' => true ),
'line' => $common + array( 'x1' => true, 'y1' => true, 'x2' => true, 'y2' => true ),
'polyline' => $common + array( 'points' => true ),
);
}
/**
* Return a sanitised icon, or an empty string if it does not exist.
*
* @param string $name File name without extension, [a-z0-9-] only.
* @param string $label Accessible name; empty marks the icon decorative.
*/
function acme_icon( string $name, string $label = '' ): string {
$name = sanitize_file_name( $name );
if ( ! preg_match( '/^[a-z0-9-]+$/', $name ) ) {
return '';
}
$cache_key = 'acme_icon_' . $name;
$svg = wp_cache_get( $cache_key, 'acme' );
if ( false === $svg ) {
$file = get_template_directory() . '/assets/icons/' . $name . '.svg';
$raw = is_readable( $file ) ? (string) file_get_contents( $file ) : '';
$svg = wp_kses( $raw, acme_svg_allowed_html() );
wp_cache_set( $cache_key, $svg, 'acme' );
}
if ( '' === $svg ) {
return '';
}
$attrs = '' === $label
? ' aria-hidden="true" focusable="false"'
: ' role="img" aria-label="' . esc_attr( $label ) . '"';
return preg_replace( '/^<svg/', '<svg' . $attrs, $svg, 1 );
}
// Template use: echo acme_icon( 'arrow-right' );
// Output is already sanitised, so echo it directly.
How it works
What it solves
Inline SVG icons are crisp and styleable with currentColor, but an SVG is a document that can carry scripts and event handlers. Reading arbitrary files and echoing them is a cross-site scripting risk, so every icon should pass through a strict allow-list.
How it works
acme_icon() accepts a name, normalises it with sanitize_file_name, and insists on lowercase letters, digits and dashes so path traversal is impossible. The file is read from the theme and filtered by wp_kses with a short list of drawing elements and attributes; script, foreignObject and on* handlers are not on it and disappear. The result is cached in the object cache and given either aria-hidden or an accessible label.
The returned string can be echoed directly.
Adapting it
Extend the allow-list only with elements you actually use, such as defs and linearGradient. Make icons inherit colour by using currentColor in the files. Add a size parameter that sets width and height. To support uploads, apply the same sanitiser at upload time and never display unsanitised files.
Pitfalls
wp_kseslowercases attribute names, which is whyviewboxis written that way in the list.- Do not allow
styleorhrefwithout extra care; they can carry URLs. - The object cache is per request without a persistent backend; that is fine.
- Remove XML headers and comments from icon files.
How to use it
Save as inc/icons.php, require it from functions.php, and put files in assets/icons/.
Use echo acme_icon( 'arrow-right' ); for a decorative icon.
Use echo acme_icon( 'search', 'Search' ); when the icon stands alone and needs a name.
Licence: GPL-2.0-or-later. This snippet comes from WordPress code and is GPL-2.0-or-later.