GHSA-5CWG-6VW7-RJ6P

Vulnerability from github – Published: 2026-10-30 23:02 – Updated: 2026-10-30 23:02
VLAI
Summary
Forge: AES-GCM authentication bypass via unvalidated tagLength option
Details

Summary

The AES-GCM mode in forge.cipher does not validate the tagLength option passed to start(). With a tagLength of 0 or a value that converts to 0 (such as null, false, '' or '0'), the authentication tag is empty, and decipher.finish() returns true for any ciphertext and additional data when the supplied tag is empty or missing. Fixed in node-forge 1.4.1.

Details

modes.gcm.prototype.start() in lib/cipherModes.js stored the tagLength option without validation, and lib/cipher.js documented it as accepting "0-128". After processing the data, afterFinish() truncates the 16-byte tag with truncate(16 % (tagLength / 8)). For a tagLength that converts to 0, this is 16 % 0 = NaN, and truncating by NaN empties the tag. On decryption, the start-time check that the supplied tag has tagLength / 8 bytes passes for an empty tag, and the final tag comparison compares two empty strings, so decipher.finish() returns true for any ciphertext and additional data.

NIST SP 800-38D section 5.2.1.2 allows only tag lengths of 128, 120, 112, 104 and 96 bits, plus 64 and 32 bits for certain applications.

The fix validates tagLength in start(), for both encryption and decryption. A missing, null or undefined tagLength uses the default of 128 bits. Any other value is converted with Number() and must be one of 128, 120, 112, 104, 96, 64 or 32; otherwise start() throws "Invalid tag length.". Numeric strings such as '128' are still accepted. With a validated tagLength, an empty tag no longer matches the expected tag length and is rejected.

Patches

Fixed in node-forge 1.4.1. cipher.start() now throws an error unless tagLength is one of the tag lengths allowed by NIST SP 800-38D (128, 120, 112, 104, 96, 64 or 32 bits); a null or undefined tagLength means the default of 128 bits.

Workarounds

Do not derive tagLength from received data. Pass a fixed tagLength (or omit it to use the default of 128 bits), and check that the received tag has the expected length before calling start(), for example if(tag.length !== 16) { throw new Error('Invalid tag.'); } before decipher.start({iv: iv, tag: tag});.

Proof of concept

Start an AES-GCM decipher with tagLength: 0 and an empty tag, as an application does when it derives the tag length from a received empty tag, and feed it arbitrary bytes that were never encrypted under the key:

var decipher = forge.cipher.createDecipher('AES-GCM', key);
decipher.start({iv: iv, tag: '', tagLength: 0});
decipher.update(forge.util.createBuffer('any attacker-chosen bytes'));
decipher.finish();
// <= 1.4.0: true
// 1.4.1: start() throws "Invalid tag length."

tagLength values null, false and '0' (with an empty tag), and tagLength: 0 with no tag option, give the same result on <= 1.4.0. The fix adds regression tests in tests/unit/aes.js.

Impact

Applications that decrypt AES-GCM data with forge.cipher.createDecipher() ('AES-GCM') and pass a zero-like tagLength are affected. Typical cases are a tag length derived from the received tag, or an unset configuration value:

var decipher = forge.cipher.createDecipher('AES-GCM', key);
decipher.start({iv: iv, tag: receivedTag, tagLength: receivedTag.length * 8});
decipher.update(forge.util.createBuffer(receivedCiphertext));
if(decipher.finish()) { /* attacker-supplied data accepted */ }

An attacker who can send a ciphertext with an empty tag to such an application can make it accept attacker-chosen ciphertext as authentic. Because GCM encryption is a stream cipher, an attacker who knows (part of) a captured message's plaintext can also change that plaintext to chosen values. The default (no tagLength option, 128 bits) and any fixed valid tagLength are not affected.

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 1.4.0"
      },
      "package": {
        "ecosystem": "npm",
        "name": "node-forge"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "1.4.1"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-119186"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-1284",
      "CWE-354"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-10-30T23:02:09Z",
    "nvd_published_at": "2026-10-29T20:17:29Z",
    "severity": "MODERATE"
  },
  "details": "### Summary\n\nThe AES-GCM mode in forge.cipher does not validate the tagLength option passed to start(). With a tagLength of 0 or a value that converts to 0 (such as null, false, \u0027\u0027 or \u00270\u0027), the authentication tag is empty, and decipher.finish() returns true for any ciphertext and additional data when the supplied tag is empty or missing. Fixed in node-forge 1.4.1.\n\n### Details\n\n`modes.gcm.prototype.start()` in `lib/cipherModes.js` stored the `tagLength` option without validation, and `lib/cipher.js` documented it as accepting \"0-128\". After processing the data, `afterFinish()` truncates the 16-byte tag with `truncate(16 % (tagLength / 8))`. For a `tagLength` that converts to 0, this is `16 % 0 = NaN`, and truncating by `NaN` empties the tag. On decryption, the start-time check that the supplied `tag` has `tagLength / 8` bytes passes for an empty tag, and the final tag comparison compares two empty strings, so `decipher.finish()` returns `true` for any ciphertext and additional data.\n\nNIST SP 800-38D section 5.2.1.2 allows only tag lengths of 128, 120, 112, 104 and 96 bits, plus 64 and 32 bits for certain applications.\n\nThe fix validates `tagLength` in `start()`, for both encryption and decryption. A missing, `null` or `undefined` `tagLength` uses the default of 128 bits. Any other value is converted with `Number()` and must be one of 128, 120, 112, 104, 96, 64 or 32; otherwise `start()` throws \"Invalid tag length.\". Numeric strings such as `\u0027128\u0027` are still accepted. With a validated `tagLength`, an empty tag no longer matches the expected tag length and is rejected.\n\n### Patches\nFixed in node-forge 1.4.1. `cipher.start()` now throws an error unless `tagLength` is one of the tag lengths allowed by NIST SP 800-38D (128, 120, 112, 104, 96, 64 or 32 bits); a `null` or `undefined` `tagLength` means the default of 128 bits.\n\n### Workarounds\nDo not derive `tagLength` from received data. Pass a fixed `tagLength` (or omit it to use the default of 128 bits), and check that the received tag has the expected length before calling `start()`, for example `if(tag.length !== 16) { throw new Error(\u0027Invalid tag.\u0027); }` before `decipher.start({iv: iv, tag: tag});`.\n\n### Proof of concept\n\nStart an AES-GCM decipher with `tagLength: 0` and an empty tag, as an application does when it derives the tag length from a received empty tag, and feed it arbitrary bytes that were never encrypted under the key:\n\n```js\nvar decipher = forge.cipher.createDecipher(\u0027AES-GCM\u0027, key);\ndecipher.start({iv: iv, tag: \u0027\u0027, tagLength: 0});\ndecipher.update(forge.util.createBuffer(\u0027any attacker-chosen bytes\u0027));\ndecipher.finish();\n// \u003c= 1.4.0: true\n// 1.4.1: start() throws \"Invalid tag length.\"\n```\n\n`tagLength` values `null`, `false` and `\u00270\u0027` (with an empty tag), and `tagLength: 0` with no `tag` option, give the same result on \u003c= 1.4.0. The fix adds regression tests in `tests/unit/aes.js`.\n\n### Impact\n\nApplications that decrypt AES-GCM data with `forge.cipher.createDecipher()` (`\u0027AES-GCM\u0027`) and pass a zero-like `tagLength` are affected. Typical cases are a tag length derived from the received tag, or an unset configuration value:\n\n```js\nvar decipher = forge.cipher.createDecipher(\u0027AES-GCM\u0027, key);\ndecipher.start({iv: iv, tag: receivedTag, tagLength: receivedTag.length * 8});\ndecipher.update(forge.util.createBuffer(receivedCiphertext));\nif(decipher.finish()) { /* attacker-supplied data accepted */ }\n```\n\nAn attacker who can send a ciphertext with an empty tag to such an application can make it accept attacker-chosen ciphertext as authentic. Because GCM encryption is a stream cipher, an attacker who knows (part of) a captured message\u0027s plaintext can also change that plaintext to chosen values. The default (no `tagLength` option, 128 bits) and any fixed valid `tagLength` are not affected.",
  "id": "GHSA-5cwg-6vw7-rj6p",
  "modified": "2026-10-30T23:02:09Z",
  "published": "2026-10-30T23:02:09Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/digitalbazaar/forge/security/advisories/GHSA-5cwg-6vw7-rj6p"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-119186"
    },
    {
      "type": "WEB",
      "url": "https://github.com/digitalbazaar/forge/commit/236845d96c71207259f91638d0138bba8e2fd624"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/digitalbazaar/forge"
    },
    {
      "type": "WEB",
      "url": "https://github.com/digitalbazaar/forge/releases/tag/v1.4.1"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:H/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Forge: AES-GCM authentication bypass via unvalidated tagLength option"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Drag nodes to arrange them. Double-click a related vulnerability to open it, or a source to visit its link. The Physics button on the left tunes the layout and the simulation; the legend at the bottom left hides or shows nodes by kind and by sighting type.

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.
Vulnerability timeline

Sightings and recorded dates, grouped by day · UTC

JSON
Events included in the timeline

Loading timeline…


    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.