Skip to content

Security & Performance

When to Use

Use this guide when hardening taxonomy implementations against security vulnerabilities and optimizing for large-scale performance.

Security

Access Control

Vocabulary-level permissions:

// GOOD: Granular per-vocabulary permissions
$account->hasPermission("edit terms in $vid");

// BAD: Overly permissive
$account->hasPermission('administer taxonomy');

Term view access: - Terms require access content permission AND published status - Unpublished terms only visible to users with administer taxonomy - No per-term access control in core — use contrib Permissions by Term for fine-grained control

XSS prevention:

// ALWAYS sanitize term names in custom output
$safe_name = Html::escape($term->getName());

// Twig auto-escapes; raw output requires |raw filter
{{ term.name }} {# Safe #}
{{ term.name|raw }} {# Dangerous unless sanitized #}

Auto-create validation:

// Prevent spam/XSS in auto-created terms
function mymodule_taxonomy_term_presave(Term $term) {
  if ($term->isNew()) {
    $name = $term->getName();
    // Enforce max length
    if (strlen($name) > 50) {
      $term->setName(substr($name, 0, 50));
    }
    // Strip HTML tags
    $term->setName(strip_tags($name));
    // Normalize whitespace
    $term->setName(preg_replace('/\s+/', ' ', trim($name)));
  }
}

SQL injection: - Entity queries are parameterized — safe by default - NEVER build raw SQL with term names: $db->query("SELECT * FROM node WHERE title LIKE '%{$term->getName()}%'") → Use entity query or placeholders

CSRF protection: - Term edit/delete forms include CSRF tokens automatically - Custom forms must use Form API for CSRF protection

Performance

Query Optimization

N+1 query problem:

// BAD: N+1 queries
foreach ($nodes as $node) {
  $terms = $node->get('field_tags')->referencedEntities();
  // Queries database for each node
}

// GOOD: Preload all referenced terms
$tids = [];
foreach ($nodes as $node) {
  foreach ($node->get('field_tags') as $item) {
    $tids[] = $item->target_id;
  }
}
$terms = $term_storage->loadMultiple(array_unique($tids));

loadTree() optimization:

// BAD: Out-of-memory with 10k+ terms
$tree = $term_storage->loadTree($vid, 0, NULL, TRUE);

// GOOD: Load lightweight objects, cherry-pick entities
$tree = $term_storage->loadTree($vid, 0, NULL, FALSE);
$tids_to_load = array_slice(array_column($tree, 'tid'), 0, 100);
$terms = $term_storage->loadMultiple($tids_to_load);

Caching term trees:

// Cache tree for 1 hour
$cid = "taxonomy_tree:$vid";
$cache = \Drupal::cache()->get($cid);

if ($cache) {
  $tree = $cache->data;
} else {
  $tree = $term_storage->loadTree($vid);
  \Drupal::cache()->set($cid, $tree, time() + 3600, ["taxonomy_term_list:$vid"]);
}

Index optimization: - taxonomy_index table indexes nid, tid, and composite (nid, tid) - For custom entity types, add index on term reference field: indexes: { target_id: ['target_id'] } - Views taxonomy filters use taxonomy_index for fast lookups

Scalability Thresholds

Term Count Performance Impact Mitigation
<100 Negligible Use default approaches
100-1,000 loadTree() slows; dropdown widgets lag Cache trees; use autocomplete widgets
1,000-10,000 loadTree() with load_entities causes memory issues Use load_entities = FALSE; paginate admin UI
>10,000 Admin UI fails; exposed filters timeout Disable overview form; use Search API/Solr for faceting

Large vocabulary strategies: - Disable term overview page: hook_entity_operation_alter() to remove "List terms" link - Use autocomplete everywhere: never render full term list - Consider hierarchical facets in Search API instead of Views exposed filters - Partition large vocabularies: "US States", "Canadian Provinces" instead of "All Regions"

Common Mistakes

  • Trusting user input in auto-created terms → Allows XSS, spam, database bloat. Always validate and sanitize in presave hook
  • Not setting cache tags on term-dependent data → Stale data when terms update. Use ['taxonomy_term:' . $tid] cache tag
  • Exposing term overview form for large vocabularies → Page times out loading >5k terms. Restrict access or provide filtered views instead
  • Using taxonomy_index for non-nodes → Only nodes are indexed. Use entity reference queries or build custom index table
  • Not invalidating term tree cache → Outdated hierarchy after term save. Use cache tag taxonomy_term_list:$vid and invalidate on term changes
  • Granting 'administer taxonomy' to untrusted roles → Allows term deletion, vocabulary structure changes, permission escalation. Use per-vocabulary permissions

See Also