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:$vidand invalidate on term changes - Granting 'administer taxonomy' to untrusted roles → Allows term deletion, vocabulary structure changes, permission escalation. Use per-vocabulary permissions
See Also
- ← Previous: Anti-Patterns & Common Mistakes | Next: Code Reference Map →
- Reference: Drupal.org Issue: DB caching for loadTree()
- Reference: Seth Shaw: Large Vocab loadTree Error
- Reference: Drupal.org Permissions by Term module