Skip to main content
Home
Drupal life hacks

Main navigation

  • Drupal
  • React
  • WP
  • Contact
  • About
User account menu
  • Log in

Breadcrumb

  1. Home

Why Direct Database Queries in Drupal Are Dangerous: Understanding Entity Access and Real-World Failures

By admin, 28 November, 2025

When working with Drupal entities, many developers assume that access control “just works.” This is only partially true. Drupal does provide a robust access system — but only when you interact with entities using the Entity API.

However, if you run raw SQL queries, write custom EntityQuery logic, or misconfigure Views, you can accidentally expose sensitive content to unauthorized users.

This article explains exactly why this happens, how it affects production systems, and how to fix it — with real code examples and real-world production incidents.


🔒 1. How Entity Access in Drupal Really Works

Drupal enforces entity access through:

  • Entity Access Control Handlers

  • hook_entity_access()

  • hook_node_access()

  • Permissions + workflows (e.g., Content Moderation, Workbench)

  • Field-level access checkers

These checks activate automatically only when:

✔ You load a full entity via storage

 
$node = \Drupal::entityTypeManager()
  ->getStorage('node')
  ->load($nid);

if ($node->access('view')) {
  // Access allowed
}

✔ You render the entity as a whole

 
$build = \Drupal::entityTypeManager()
  ->getViewBuilder('node')
  ->view($node);

If you do anything else — Drupal assumes you know what you're doing, and access checks DO NOT run.


❗ 2. EntityQuery Does NOT Check Access (Common Developer Trap)

A typical mistake:

 
$query = \Drupal::entityQuery('node')
  ->condition('type', 'article');

$nids = $query->execute(); // Contains ALL article nodes 

Even unpublished or restricted nodes will appear here.

✔ Safe version

 
$storage = \Drupal::entityTypeManager()->getStorage('node');
$nodes = $storage->loadMultiple($nids);

$visible_nodes = array_filter($nodes, function ($node) {
  return $node->access('view');
});

❗ 3. SQL Queries Bypass ALL Security

Example:

 
$result = \Drupal::database()->select('node_field_data', 'n')
  ->fields('n', ['nid', 'title', 'status'])
  ->execute()
  ->fetchAll();

No access checks. Ever.
This query returns:

  • unpublished nodes,

  • restricted nodes (private workspaces),

  • content the user should never know exists.

✔ Safe alternative

Use entity storage instead of SQL.


❗ 4. Views Can Leak Content Too

Many developers believe Views is always safe.

Это не так.

Views builds SQL queries manually.
If a site admin removes access filters “to make something work,” Drupal has no way of knowing.

Example of dangerous Views setup

  • Access → “Unrestricted”

  • Display only fields (not full entities)

  • Disabled “Published status” filter

  • Custom relationship that bypasses node grants

In this case, Views may show:

  • titles of private content,

  • field values from restricted nodes,

  • unpublished content to anonymous users.

This caused multiple production leaks across real companies (examples ниже).


❗ 5. Rendering Fields Manually Bypasses Access

Example:

 
$value = $node->get('field_secret')->value;

If you didn't check $node->access('view') before this, you may reveal sensitive information.

✔ Safe version:

 
if ($node->access('view')) {
  $value = $node->get('field_secret')->value;
}

🔥 REAL PRODUCTION CASES (from actual Drupal projects)

🚨 Case 1: Medical Portal Leaking Unpublished Data

A Drupal 9 site for a medical clinic used a custom report:

 
$ids = \Drupal::entityQuery('node')
  ->condition('type', 'patient_record')
  ->execute();

Doctors with limited access suddenly saw all patient records, including those assigned to other departments.

Cause: No access checks.

Fix: Added ->access('view') filtering and replaced EntityQuery with proper storage loading.


🚨 Case 2: University Portal Exposing Student Applications

A Views page listed student submissions.
An administrator disabled the access handler because “it hid some rows.”

Result:
Students could see titles of applications belonging to other users (private documents).

Cause: Views access settings disabled + displaying fields instead of nodes.

Fix:
Enabled Views access checks + switched to rendering the entire entity.


🚨 Case 3: E-commerce Store Displaying Hidden Products

A custom SQL query fetched product prices:

 
SELECT sku, price FROM commerce_product_field_data;

The marketing team used it in a dashboard.
Guest users who accessed the dashboard endpoint accidentally received prices for discontinued items, including confidential wholesale pricing.

Cause: Direct SQL without entity access.


✔ Best Practices to Avoid Security Failures

1. Always load entities through the Entity API

Never rely on raw SQL for content.

2. After EntityQuery — always check access

Access is not part of EntityQuery.

3. Avoid direct field rendering

Render the whole entity instead.

4. Configure Views correctly

Ensure:

  • Access → “Permission”

  • “Published” filter is enabled

  • Do not remove access handlers to “fix visibility issues”

5. Use view builders whenever possible

They perform automatic access checks.


🧾 Conclusion

Drupal’s access system is powerful, but only if you work within its framework.
By using:

  • EntityQuery without checks

  • SQL queries

  • Misconfigured Views

  • Direct field extraction

…you bypass access control entirely, risking severe data leaks.

The golden rule:

⭐ If you didn’t load the entity through storage, Drupal did not check access.

Follow the best practices above to keep your Drupal applications secure and predictable — especially at scale.

 

Avoiding Entity Access Pitfalls in Drupal: Extended Guide with Code Examples

This extended version includes deeper technical details and additional code samples demonstrating common mistakes and their secure alternatives.


1. EntityQuery Does Not Check Access

❌ Incorrect: assuming EntityQuery filters out inaccessible entities

$query = \Drupal::entityQuery('node')
  ->condition('type', 'article')
  ->condition('status', 1);

$nids = $query->execute();

// Developer mistakenly believes these nodes are all visible to the user.
foreach ($nids as $nid) {
  $node = \Drupal::entityTypeManager()->getStorage('node')->load($nid);
  // Rendering content without checking access.
  echo $node->title->value;
}

Even if status = 1 (published), moderated content or restricted nodes will still appear.

✔ Correct: recheck access after loading entities

$storage = \Drupal::entityTypeManager()->getStorage('node');

$nodes = $storage->loadMultiple(
  \Drupal::entityQuery('node')
    ->condition('type', 'article')
    ->execute()
);

foreach ($nodes as $node) {
  if ($node->access('view')) {
    echo $node->label();
  }
}

A standard and safe pattern.


2. Direct SQL Queries Bypass Access Control

❌ Incorrect: retrieving data from database tables directly

$results = \Drupal::database()->select('node_field_data', 'n')
  ->fields('n', ['nid', 'title', 'uid', 'status'])
  ->execute()
  ->fetchAll();

foreach ($results as $row) {
  // User may see private, restricted, or unpublished nodes
  print $row->title;
}

SQL knows nothing about Drupal permissions.


✔ Correct: use SQL only for non-sensitive operations

If you must use SQL, then load entities afterward:

$ids = \Drupal::database()->select('node_field_data', 'n')
  ->fields('n', ['nid'])
  ->execute()
  ->fetchCol();

$storage = \Drupal::entityTypeManager()->getStorage('node');
$nodes = $storage->loadMultiple($ids);

foreach ($nodes as $node) {
  if ($node->access('view')) {
    print $node->label();
  }
}

Now SQL is only used for fetching IDs; access is still enforced.


3. Access Problems in Views

Views internally uses SQL queries and can easily expose inaccessible data if configured incorrectly.

❌ Incorrect: Views displaying fields without access protection

A typical risk:

  • Display type: Fields
  • Access: None
  • Filter: Content: Published = Yes

A user without permission to view unpublished content might still see:

  • Titles
  • Author names
  • Custom fields

✔ Correct: enforce access checks inside Views

Recommended settings:

  • Access: Permissions: View published content
  • Query settings → Disable SQL rewrite: OFF
  • Render: Content (Rendered entity) instead of individual fields

Additionally, a custom Views filter can enforce access:

public function query() {
  $this->ensureMyTable();

  $this->query->addWhereExpression(0, "1 = 1"); // placeholder

  // Enforce built-in access checks.
  $this->query->addTag('node_access');
}

The tag ensures Drupal rewrites the SQL query to apply node access rules.


4. Rendering Fields Without Checking Entity Access

❌ Incorrect: reading field values directly

$node = \Drupal::entityTypeManager()->getStorage('node')->load($nid);

$value = $node->get('field_secret')->value;

print $value;

If the user can load the node ID (through EntityQuery or otherwise), they might see sensitive fields.


✔ Correct: check entity access before field access

$node = \Drupal::entityTypeManager()->getStorage('node')->load($nid);

if ($node && $node->access('view')) {
  echo $node->get('field_secret')->value;
}

✔ Or render with view builder (access enforced automatically)

$build = \Drupal::entityTypeManager()
  ->getViewBuilder('node')
  ->view($node, 'teaser');

return $build;

Render arrays automatically apply access checks.


5. Example: Secure Entity Loading Pattern (Reusable)

A recommended reusable helper:

/**
 * Loads viewable entities of a given type based on an entity query.
 */
function load_viewable_entities($entity_type_id, array $conditions) {
  $query = \Drupal::entityQuery($entity_type_id);

  foreach ($conditions as $field => $value) {
    $query->condition($field, $value);
  }

  $ids = $query->execute();

  $storage = \Drupal::entityTypeManager()->getStorage($entity_type_id);
  $entities = $storage->loadMultiple($ids);

  return array_filter($entities, function ($entity) {
    return $entity->access('view');
  });
}

Usage:

$articles = load_viewable_entities('node', ['type' => 'article']);

6. Example: Protecting Custom Controllers and Routes

❌ Incorrect: returning content directly

public function listArticles() {
  $nids = \Drupal::entityQuery('node')
    ->condition('type', 'article')
    ->execute();

  return [
    '#theme' => 'item_list',
    '#items' => array_map(function ($nid) {
      $node = Node::load($nid);
      return $node->label();
    }, $nids),
  ];
}

This exposes private nodes if access isn’t enforced.


✔ Correct: enforce access before rendering

public function listArticles() {
  $storage = \Drupal::entityTypeManager()->getStorage('node');
  
  $nids = \Drupal::entityQuery('node')
    ->condition('type', 'article')
    ->execute();

  $nodes = $storage->loadMultiple($nids);

  $visible = array_filter($nodes, fn($node) => $node->access('view'));

  return [
    '#theme' => 'item_list',
    '#items' => array_map(fn($node) => $node->label(), $visible),
  ];
}

Final Summary

This extended article demonstrates:

  • Why EntityQuery must be combined with access checks.
  • Why SQL-based fetching is dangerous.
  • How Views can silently expose content.
  • How to enforce secure rendering patterns.
  • Best practices for controllers, render arrays, and helper functions.

Tags

  • Drupal
  • Entity Access
  • Access Control
  • EntityQuery
  • Drupal security
  • Views
  • SQL Queries

Comments

About text formats

Restricted HTML

  • Allowed HTML tags: <a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>
  • Lines and paragraphs break automatically.
  • Web page addresses and email addresses turn into links automatically.
Powered by Drupal