GHSA-RW77-VQ4G-X3HP

Vulnerability from github – Published: 2026-09-24 19:30 – Updated: 2026-09-24 19:30
VLAI
Summary
phpMyFAQ has SQL Injection in `StopWords::add()` — Unescaped Stop Word Insertion
Details

Summary

The StopWords::add() method in phpMyFAQ builds a SQL INSERT statement using sprintf() and inserts the user-supplied stop word value directly into the query string without calling the application's database escaping function on it. A sibling method, StopWords::update(), which modifies an existing stop word, correctly escapes the same kind of input. The omission is isolated to the add() (insert) code path.

An authenticated administrator who can reach the stop-word management feature can submit a crafted value as the "word" parameter that breaks out of the SQL string literal and injects arbitrary SQL, including statements to drop tables, exfiltrate data, or modify other rows in the database.


Affected Code

File: phpmyfaq/src/phpMyFAQ/StopWords.php Method: add() (approx. lines 60–75 in the audited revision)

$sql = sprintf(
    "INSERT INTO %s VALUES(%d, '%s', '%s')",
    $this->getTableName(),
    $id,
    $this->configuration->getDb()->escape($this->language), // language IS escaped
    $word                                                     // <-- $word is NOT escaped
);

$word is taken directly from the administrative form input (the new stop word to add) and concatenated into the SQL string via sprintf("'%s'", ...) with no call to the database driver's escape() method.

Contrast with the safe sibling method

Method: update() (line 82 in the audited revision)

$this->configuration->getDb()->escape($word)

update() — which modifies an existing stop word — correctly escapes $word before use. add() does not perform the same escaping on the equivalent value. This inconsistency between two methods handling the same data type is the root cause: the escaping convention used throughout the rest of the file was not applied uniformly to this one insertion path.


Proof of Concept

Precondition: Attacker has valid administrator credentials (or has otherwise obtained an authenticated administrator session, e.g. via a separate session-hijacking or CSRF vector).

Attack steps:

  1. Authenticate to the phpMyFAQ administration panel.
  2. Navigate to the Stop Words management feature.
  3. Submit a new stop word with the following value instead of a normal word:

test', 'en'); DROP TABLE faqstopwords; --

  1. The resulting SQL statement sent to the database becomes (table/column names approximate, based on the traced sprintf template):

sql INSERT INTO faqstopwords VALUES(1, 'en', 'test', 'en'); DROP TABLE faqstopwords; --')

  1. The injected DROP TABLE faqstopwords; statement executes as a second SQL statement (subject to the database driver/PDO configuration permitting multi-statement execution; even where multi-statement execution is disabled, the same injection point allows classic single-statement SQLi techniques such as UNION-based data extraction or boolean/time-based blind injection against other tables the database user can access).

Impact

  • Confidentiality: An attacker with this access can use UNION-based or blind SQL injection techniques to read data from other tables in the database (e.g. user credentials, FAQ content marked as private/internal, session data) that the database user account has permission to access.
  • Integrity: Arbitrary INSERT/UPDATE/DELETE statements can be appended, allowing modification of unrelated application data.
  • Availability: As demonstrated in the PoC, structural statements like DROP TABLE can be injected, directly impacting application availability.

Mitigating factor: Exploitation requires an authenticated administrator session. This is not exploitable by an anonymous or low-privilege user. This lowers the severity from Critical/High to Medium, consistent with phpMyFAQ's own threat model where administrators are a trusted role — but it remains a genuine defense-in-depth failure: a compromised or malicious admin account (or an admin tricked via a separate vector such as CSRF, if no CSRF protection exists on this specific form) can leverage this into full database compromise, which a properly parameterized query would have prevented even in that scenario.


Root Cause

The codebase's established pattern for this class (StopWords.php) is to escape all string values via $this->configuration->getDb()->escape($value) before placing them into a sprintf()-built SQL string. This pattern is correctly applied to:

  • $this->language in add()
  • $word in update()

It is not applied to $word in add(). This is a single-line omission, not a structural design flaw — the safe pattern already exists in the same file and the same class, just inconsistently applied across the two methods that handle the same input type.


Recommended Fix

Apply the same escaping already used in update() and already used for $this->language in the same add() method:

$sql = sprintf(
    "INSERT INTO %s VALUES(%d, '%s', '%s')",
    $this->getTableName(),
    $id,
    $this->configuration->getDb()->escape($this->language),
    $this->configuration->getDb()->escape($word)   // FIX: escape $word here
);

Stronger recommended fix (defense in depth): Migrate this query, and ideally all sprintf()-built SQL in this class, to parameterized/prepared statements (e.g. PDO::prepare() with bound parameters) rather than string-escaping plus sprintf(). Escaping is correct when applied consistently, but prepared statements remove this entire vulnerability class structurally and prevent any future omission of this kind from being exploitable.

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 4.1.5"
      },
      "package": {
        "ecosystem": "Packagist",
        "name": "phpmyfaq/phpmyfaq"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "4.1.6"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 4.1.5"
      },
      "package": {
        "ecosystem": "Packagist",
        "name": "thorsten/phpmyfaq"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "4.1.6"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-56738"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-89"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-09-24T19:30:07Z",
    "nvd_published_at": "2026-09-24T17:17:04Z",
    "severity": "HIGH"
  },
  "details": "## Summary\n\nThe `StopWords::add()` method in phpMyFAQ builds a SQL `INSERT` statement using `sprintf()` and inserts the user-supplied stop word value directly into the query string **without calling the application\u0027s database escaping function** on it. A sibling method, `StopWords::update()`, which modifies an *existing* stop word, correctly escapes the same kind of input. The omission is isolated to the `add()` (insert) code path.\n\nAn authenticated administrator who can reach the stop-word management feature can submit a crafted value as the \"word\" parameter that breaks out of the SQL string literal and injects arbitrary SQL, including statements to drop tables, exfiltrate data, or modify other rows in the database.\n\n---\n\n## Affected Code\n\n**File:** `phpmyfaq/src/phpMyFAQ/StopWords.php`\n**Method:** `add()` (approx. lines 60\u201375 in the audited revision)\n\n```php\n$sql = sprintf(\n    \"INSERT INTO %s VALUES(%d, \u0027%s\u0027, \u0027%s\u0027)\",\n    $this-\u003egetTableName(),\n    $id,\n    $this-\u003econfiguration-\u003egetDb()-\u003eescape($this-\u003elanguage), // language IS escaped\n    $word                                                     // \u003c-- $word is NOT escaped\n);\n```\n\n`$word` is taken directly from the administrative form input (the new stop word to add) and concatenated into the SQL string via `sprintf(\"\u0027%s\u0027\", ...)` with no call to the database driver\u0027s `escape()` method.\n\n### Contrast with the safe sibling method\n\n**Method:** `update()` (line 82 in the audited revision)\n\n```php\n$this-\u003econfiguration-\u003egetDb()-\u003eescape($word)\n```\n\n`update()` \u2014 which modifies an existing stop word \u2014 correctly escapes `$word` before use. `add()` does not perform the same escaping on the equivalent value. This inconsistency between two methods handling the same data type is the root cause: the escaping convention used throughout the rest of the file was not applied uniformly to this one insertion path.\n\n---\n\n## Proof of Concept\n\n**Precondition:** Attacker has valid administrator credentials (or has otherwise obtained an authenticated administrator session, e.g. via a separate session-hijacking or CSRF vector).\n\n**Attack steps:**\n\n1. Authenticate to the phpMyFAQ administration panel.\n2. Navigate to the Stop Words management feature.\n3. Submit a new stop word with the following value instead of a normal word:\n\n   ```\n   test\u0027, \u0027en\u0027); DROP TABLE faqstopwords; --\n   ```\n\n4. The resulting SQL statement sent to the database becomes (table/column names approximate, based on the traced `sprintf` template):\n\n   ```sql\n   INSERT INTO faqstopwords VALUES(1, \u0027en\u0027, \u0027test\u0027, \u0027en\u0027); DROP TABLE faqstopwords; --\u0027)\n   ```\n\n5. The injected `DROP TABLE faqstopwords;` statement executes as a second SQL statement (subject to the database driver/PDO configuration permitting multi-statement execution; even where multi-statement execution is disabled, the same injection point allows classic single-statement SQLi techniques such as `UNION`-based data extraction or boolean/time-based blind injection against other tables the database user can access).\n\n---\n\n## Impact\n\n- **Confidentiality:** An attacker with this access can use UNION-based or blind SQL injection techniques to read data from other tables in the database (e.g. user credentials, FAQ content marked as private/internal, session data) that the database user account has permission to access.\n- **Integrity:** Arbitrary `INSERT`/`UPDATE`/`DELETE` statements can be appended, allowing modification of unrelated application data.\n- **Availability:** As demonstrated in the PoC, structural statements like `DROP TABLE` can be injected, directly impacting application availability.\n\n**Mitigating factor:** Exploitation requires an authenticated administrator session. This is not exploitable by an anonymous or low-privilege user. This lowers the severity from Critical/High to Medium, consistent with phpMyFAQ\u0027s own threat model where administrators are a trusted role \u2014 but it remains a genuine defense-in-depth failure: a compromised or malicious admin account (or an admin tricked via a separate vector such as CSRF, if no CSRF protection exists on this specific form) can leverage this into full database compromise, which a properly parameterized query would have prevented even in that scenario.\n\n---\n\n## Root Cause\n\nThe codebase\u0027s established pattern for this class (`StopWords.php`) is to escape all string values via `$this-\u003econfiguration-\u003egetDb()-\u003eescape($value)` before placing them into a `sprintf()`-built SQL string. This pattern is correctly applied to:\n\n- `$this-\u003elanguage` in `add()`\n- `$word` in `update()`\n\nIt is **not** applied to `$word` in `add()`. This is a single-line omission, not a structural design flaw \u2014 the safe pattern already exists in the same file and the same class, just inconsistently applied across the two methods that handle the same input type.\n\n---\n\n## Recommended Fix\n\nApply the same escaping already used in `update()` and already used for `$this-\u003elanguage` in the same `add()` method:\n\n```php\n$sql = sprintf(\n    \"INSERT INTO %s VALUES(%d, \u0027%s\u0027, \u0027%s\u0027)\",\n    $this-\u003egetTableName(),\n    $id,\n    $this-\u003econfiguration-\u003egetDb()-\u003eescape($this-\u003elanguage),\n    $this-\u003econfiguration-\u003egetDb()-\u003eescape($word)   // FIX: escape $word here\n);\n```\n\n**Stronger recommended fix (defense in depth):** Migrate this query, and ideally all `sprintf()`-built SQL in this class, to parameterized/prepared statements (e.g. `PDO::prepare()` with bound parameters) rather than string-escaping plus `sprintf()`. Escaping is correct when applied consistently, but prepared statements remove this entire vulnerability class structurally and prevent any future omission of this kind from being exploitable.",
  "id": "GHSA-rw77-vq4g-x3hp",
  "modified": "2026-09-24T19:30:07Z",
  "published": "2026-09-24T19:30:07Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/thorsten/phpMyFAQ/security/advisories/GHSA-rw77-vq4g-x3hp"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-56738"
    },
    {
      "type": "WEB",
      "url": "https://github.com/thorsten/phpMyFAQ/commit/d56ef5d75c3c007de095bc4c13b470c7ac783f0f"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/thorsten/phpMyFAQ"
    },
    {
      "type": "WEB",
      "url": "https://github.com/thorsten/phpMyFAQ/releases/tag/4.1.6"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N",
      "type": "CVSS_V4"
    }
  ],
  "summary": "phpMyFAQ has SQL Injection in `StopWords::add()` \u2014 Unescaped Stop Word Insertion"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Loading…

Loading…

Related by attack behaviour

Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.


Loading…