SevyDevy

Guide · Backend

Pyasn1's October 2026 Vulnerability Explained: Why Decoding Went Quadratic

21 min readUpdated By Saket Bhatnagar

Researched and drafted with AI, reviewed and edited by Saket Bhatnagar.

In short

  • The pyasn1 vulnerability is a batch of four High advisories published on 27 October 2026. pyasn1 is a pure-Python library that reads and writes ASN.1 data, a binary format used in certificates, keys, LDAP and SNMP.
  • Each bug lets a crafted message, tens or hundreds of kilobytes in size, make decoding take quadratic time: double the message and the work roughly quadruples. A server busy decoding one message cannot serve anyone else, which is a denial of service.
  • Every release up to 0.6.4 is affected by at least one of the four, per the advisories, and 0.6.5 (on PyPI since 27 October 2026) fixes all four. Many projects get pyasn1 indirectly, through rsa, ldap3, pysnmp, python-ldap or pyasn1-modules.
  • Run pip show pyasn1. If it says 0.6.4 or lower, upgrade with pip install --upgrade "pyasn1>=0.6.5" and check again inside the environment that really serves traffic.
  • Beginners can stop after the upgrade steps. Engineers get the four code paths from the 0.6.4 to 0.6.5 diff, which installed packages actually feed network bytes to the decoder, and why asyncio timeouts cannot stop it.

What goes wrong when a tiny message keeps a Python server busy for seconds?

Every other request on that worker waits behind it. The pyasn1 vulnerability fixed in version 0.6.5 is exactly this problem: crafted input made pyasn1's decoder do far more work than the input's size suggests.

pyasn1 is a Python library that turns ASN.1 bytes into Python objects and back. TLS certificates, RSA key files, LDAP directory messages and SNMP network-monitoring packets are all described in ASN.1 and stored as ASN.1 bytes. Few people install pyasn1 on purpose; it arrives as a dependency of something else.

Picture a common setup. A college IT team runs a small Python web tool for TLS certificates, the files that prove a server's identity. Staff upload one and get back its owner and expiry date. The setup is made up, but the code path is real: it parses uploads with pyasn1's DER decoder and the Certificate schema from pyasn1-modules.

Anyone who can reach the upload form can send a crafted file instead of a certificate. If one upload keeps a worker busy for seconds, a few uploads in a loop tie up every worker. Nothing is stolen, but staff renewing real certificates see timeouts.

Which words do you need before reading about the pyasn1 vulnerability?

Eight terms carry the whole guide. Each is short.

Terms used in this guide
TermWhat it means here
ASN.1Abstract Syntax Notation One: a standard way to describe structured data, like a schema language. Certificates, keys, LDAP and SNMP messages are all described in ASN.1.
BERBasic Encoding Rules: one way to turn ASN.1 data into bytes. Each value is written as a tag (what type it is), a length and the content. BER allows several ways to write the same value.
DER and CERDistinguished and Canonical Encoding Rules: stricter forms of BER that allow only one way to write each value. Certificates use DER.
Constructed encodingA BER value split into fragments, such as a long text sent as many small pieces. The decoder must glue the pieces back together.
Schema (asn1Spec)The ASN.1 type you tell pyasn1 to expect, passed as asn1Spec. Without it, pyasn1 decodes schemaless and guesses the structure from the tags.
Quadratic timeWork that grows with the square of the input size. Twice the input means about four times the work; ten times the input means about a hundred times.
Denial of service (DoS)An attack that makes a service unavailable, here by keeping its CPU busy, without reading or changing any data.
Transitive dependencyA package you did not install yourself but that another package pulled in, such as pyasn1 arriving with rsa or ldap3.

What is the pyasn1 vulnerability fixed in 0.6.5?

It is four separate quadratic-time bugs in the decoder, all rated High with a CVSS score of 7.5. All four were published on the project's GitHub security page on 27 October 2026, the same day pyasn1 0.6.5 reached PyPI.

CVSS is the common 0 to 10 severity scale. The vector on all four advisories says the attack works over a network, needs no login and no user action, and hurts availability only. Confidentiality and integrity are rated None.

The four advisories fixed in pyasn1 0.6.5 (as of 1 November 2026)
AdvisoryWhat gets slowWhich decoders
GHSA-fm9c-5rcf-2q5p (CVE-2026-119194)A container (SEQUENCE or SET) with many distinct long tags, decoded without a schemaBER, CER and DER, schemaless only
GHSA-9m75-vc3g-4x39 (CVE-2026-119195)OCTET STRING and text strings sent in many fragments; ANY values (slots that can hold any type) with no length given up front, when the schema says ANYBER and CER; DER when the schema asks for OCTET STRING, and text strings even without one
GHSA-2qm8-x737-gmw5 (CVE-2026-119196)BIT STRING sent in many fragmentsBER and CER; DER only with a schema
GHSA-mm57-4v3p-rchx (CVE-2026-119197)REAL (floating-point) numbers with a very long binary mantissaBER, CER and DER

The CVE numbers come from PyPI's vulnerability data, which lists them as aliases. The maintainer's own advisory pages still say No known CVE, so search tools may show either name. As of 1 November 2026, GitHub's Advisory Database lists GHSA-fm9c-5rcf-2q5p as CVE-2026-119194, reviewed on 30 October 2026.

This is a new batch, not a repeat of the July advisories. pyasn1 has shipped a security release roughly every quarter in 2026, each fixing a different slow or unsafe path in the same decoder.

pyasn1 security releases in 2026 (PyPI dates)
ReleaseDateWhat it fixed
0.6.216 January 2026GHSA-63vm-454h-vhhq (CVE-2026-23490): OBJECT IDENTIFIER continuation octet limits
0.6.317 March 2026GHSA-jr27-m4p2-rc6r (CVE-2026-30922): unbounded recursion, fixed with a nesting depth limit
0.6.49 July 2026Three advisories: quadratic OBJECT IDENTIFIER decoding, unbounded long tag IDs, and slow REAL to float conversion
0.6.527 October 2026The four quadratic-time advisories in this guide

What does quadratic time mean, and why does it hurt so much?

It means the work grows with the square of the input, so small inputs are fine and medium inputs are painful. Three of the four bugs share one everyday pattern: rebuilding a growing value from scratch every time a piece is added.

Think of keeping a notebook where, every time you add a line, you copy the whole notebook into a fresh one first. The notebook is the decoded value, each line is one fragment, and the copying is Python creating a new bytes object. After 1,000 lines you have copied about 500,000 lines in total.

Where the picture breaks: computers copy memory very fast, so this only shows up with thousands of pieces. That is exactly what an attacker sends.

You can see it with plain Python and no pyasn1 at all. This script builds the same bytes value twice: once with += in a loop, and once by collecting pieces in a list and joining them at the end.

python
# Harmless, generic demo: building a bytes value piece by piece.
# Nothing here touches pyasn1 or any parser.
import time

def build_with_plus(pieces):
    result = b""
    for p in pieces:
        result += p          # copies everything built so far, every time
    return result

def build_with_join(pieces):
    parts = []
    for p in pieces:
        parts.append(p)      # cheap: just remembers the piece
    return b"".join(parts)   # one copy at the end

for n in (5_000, 10_000, 20_000, 40_000):
    pieces = [b"x" * 100] * n
    t0 = time.perf_counter(); a = build_with_plus(pieces); t1 = time.perf_counter()
    b = build_with_join(pieces); t2 = time.perf_counter()
    assert a == b
    print(f"{n:>6} pieces  plus: {t1 - t0:7.3f} s   join: {t2 - t1:7.4f} s")
Output of concat_demo.py, range over four runs (CPython 3.13.18, Linux, 1 November 2026; your times will differ)
Pieces of 100 bytesBuilt with +=Built with join
5,0000.032 to 0.036 s0.0001 to 0.0002 s
10,0000.18 to 0.21 s0.0003 to 0.0004 s
20,0001.19 to 1.25 s0.0013 s
40,0005.72 to 6.20 s0.0015 to 0.0023 s

Each doubling made the += version about five to seven times slower, while join stayed in milliseconds. That is worse than the four times pure arithmetic predicts, probably because big copies stop fitting in the CPU's fast cache; we did not measure that. By arithmetic, building the 4 MB result with += copies about 80 GB of memory in total. The 0.6.5 fix for the string bug is this exact swap: a list of fragments and one join.

Repeated copying vs one join

Notice that the += path copies everything built so far on every step, while the join path copies each byte once at the end.

Diagram connections:

  • Many small fragments to result += fragment (pyasn1 0.6.4)
  • result += fragment to Quadratic total work (grows each step)
  • Many small fragments to fragments.append() (pyasn1 0.6.5)
  • fragments.append() to b''.join(fragments) (once, at the end)
  • Many small fragments: What a constructed string looks like
  • result += fragment: New object, copy all old bytes each time
  • Quadratic total work: 1 + 2 + ... + N copies
  • fragments.append(): Only remembers each piece
  • b''.join(fragments): One copy of every byte, linear work

How did the pyasn1 decoder end up doing quadratic work?

Each advisory points to loops in pyasn1/codec/ber/decoder.py that redid growing work for every item. The CER and DER decoders reuse that file, which is why one fix covers all three encodings.

Strings and ANY: copying the whole value per fragment

In 0.6.4, constructed OCTET STRING, text strings and indefinite-length ANY values were assembled with += on a growing bytes value. That is the notebook pattern from the demo. In 0.6.5 the loops append to a list and call b''.join(fragments) once.

BIT STRING and REAL: shifting a growing number

BIT STRING fragments were merged by calling fromOctetString with prepend, which shifts the whole value built so far on every fragment. 0.6.5 collects bits in a bytearray, a growable byte buffer, and converts once.

A REAL value's mantissa, the digits part of a floating-point number, was built one byte at a time. Each step shifted a growing integer and re-sliced the remaining bytes. 0.6.5 replaces the loop with a single int.from_bytes call.

Schemaless containers: a set that slows down

SEQUENCE and SET are ASN.1's container types, like a record or a list. Without a schema, pyasn1 records each element's tag in a Python set to guess which kind it is: all one type, or mixed types.

A Python set finds an item's slot from its hash, a number computed from the item. The advisory says crafted long tag IDs can produce colliding hash values, so each insert must compare against many stored items.

The code only needs to know whether there are one or two distinct tags. So 0.6.5 stops adding to the set after two, and the slowdown disappears.

Why does a slow parser become a denial of service on a server?

Because decoding is pure Python running on the CPU, and while it runs, that worker does nothing else. A request that costs the attacker a few hundred kilobytes can cost the server seconds of CPU.

The BIT STRING advisory gives real numbers from its own tests. Through the DER decoder with a schema, the path a certificate parser uses, a 0.40 MB input took 1.949 s on 0.6.4 and 0.172 s on 0.6.5.

Through BER, 0.40 MB took 1.915 s on 0.6.4 and 7.392 s at 0.80 MB. On 0.6.5 the same inputs took 0.170 s and 0.344 s. Doubling the input roughly quadrupled the old time but only doubled the new time. The advisories label these numbers local and machine-dependent, so read them as a shape, not a benchmark.

How one slow decode blocks a worker

Notice that the crafted upload and the real one share one worker. While the decoder grinds through fragments, the real upload waits in the queue.

Diagram connections:

  • Crafted upload to Python web worker (arrives first)
  • Real certificate upload to Python web worker (waits its turn)
  • Python web worker to pyasn1 DER decoder (decode(bytes))
  • pyasn1 DER decoder to Expiry check code (seconds later)
  • Crafted upload: Anyone who can reach the form → worker
  • Real certificate upload: Staff member → worker
  • Python web worker: One process or event loop
  • pyasn1 DER decoder: Pure Python, holds the CPU until done
  • Expiry check code: Runs only after decode returns

More workers only raise the number of crafted uploads needed. In an asyncio server, one slow decode blocks the event loop, the single loop that runs every task in the process. The fix has to remove the quadratic work, which is what 0.6.5 does.

We read each package's declared dependencies, the requires_dist field on PyPI, for its latest release on 1 November 2026. Being listed means pyasn1 gets installed; whether untrusted bytes reach the decoder is a separate question.

pyasn1 in PyPI metadata (latest releases, checked 1 November 2026)
Package and versionWhat it declaresDoes it feed outside bytes to pyasn1?
pyasn1-modules 0.4.2pyasn1>=0.6.1,<0.7.0It holds ready-made ASN.1 schemas; your code or another library does the decoding
rsa 4.9.1pyasn1>=0.1.3Decodes key files with the DER decoder; keys usually come from you, not strangers
google-auth 2.62.0pyasn1-modules>=0.2.1 (pyasn1 comes through it)Only its legacy python-rsa backend calls the decoder; the default path uses cryptography
ldap3 2.9.1pyasn1>=0.4.6Yes, for some LDAP server responses and controls
python-ldap 3.4.8pyasn1>=0.3.7 and pyasn1_modules>=0.1.5Yes: its ldap.controls classes decode control values sent by the LDAP server
pysnmp 7.1.30pyasn1>=0.6.4Yes: every incoming SNMP message goes to the BER decoder
impacket 0.13.1pyasn1>=0.2.3 and pyasn1_modulesYes: it decodes Kerberos and other protocol messages with pyasn1
awscli 1.46.2rsa>=3.1.2,<4.8 (pyasn1 comes through rsa)Not checked in this guide

Several well-known names do not depend on pyasn1 at all in their latest releases: cryptography, pyOpenSSL, paramiko, boto3 and service-identity. Older releases of some packages did, so check your own lock file rather than trusting this table.

The certificate example is exposed on two advisories at once. The Certificate schema in pyasn1-modules declares signatureValue as a BIT STRING and extension values as OCTET STRING, the two DER-with-schema paths in the table.

The google-auth row surprises people. Version 2.62.0 still installs pyasn1 through pyasn1-modules, but in its source the decoder is called only from google/auth/crypt/_python_rsa.py. That backend is used only when you pass it a python-rsa key object.

Am I affected by the pyasn1 vulnerability?

You are affected if pyasn1 0.6.4 or older is installed and your process decodes ASN.1 bytes that someone else controls. Both conditions matter for urgency; the first alone is enough to upgrade.

  • Highest risk: endpoints that decode large untrusted inputs with pyasn1, such as certificate, certificate request or key uploads parsed with pyasn1-modules schemas.
  • Also exposed: protocol code that decodes network messages, such as pysnmp, impacket and LDAP clients talking to servers you do not fully trust. SNMP over UDP is capped near 64 KB per message, which limits the cost per packet.
  • Lower risk: pyasn1 only installed through google-auth on its default path, or rsa loading your own key files. Upgrade anyway, because your code paths may change.
  • Using DER is not a shield. Every one of the four advisories reaches the DER decoder in some situation, as the advisory table above shows.

How do you find pyasn1 in your dependency tree?

Ask pip which version is installed, then ask which packages pulled it in. Run both inside the virtual environment or container that runs the service, not on your laptop.

bash
pip show pyasn1
# Name: pyasn1
# Version: 0.6.4
# ...
# Required-by: pyasn1_modules

pip install pipdeptree
pipdeptree -r -p pyasn1
# pyasn1==0.6.4
# └── pyasn1_modules==0.4.2 [requires: pyasn1>=0.6.1,<0.7.0]
#     └── google-auth==2.62.0 [requires: pyasn1_modules>=0.2.1]

The -r flag prints the tree in reverse, from pyasn1 up to the packages that need it. The output above is from our test environment with google-auth 2.62.0 installed; yours will list your own packages.

An audit tool can do the version check for you. pip-audit 2.9.0, run on a requirements file pinning pyasn1==0.6.4, printed Found 4 known vulnerabilities in 1 package and listed all four GHSA IDs with 0.6.5 as the fix.

How do you upgrade pyasn1 to 0.6.5 and check it worked?

Upgrade pyasn1 directly and raise the minimum in your pins. Upgrading the parent package alone may not help, because rsa, ldap3 and pyasn1-modules all accept older pyasn1 versions.

  1. Find the version

    Run pip show pyasn1 in the real environment. Anything 0.6.4 or lower is affected.

  2. Upgrade

    Run pip install --upgrade "pyasn1>=0.6.5". pyasn1 0.6.5 needs Python 3.8 or newer.

  3. Pin the minimum

    Add pyasn1>=0.6.5 to your requirements or lock file, or regenerate the lock file. A minimum pin makes a stale environment fail loudly.

  4. Check for conflicts

    Run pip check. With pyasn1-modules 0.4.2 and google-auth 2.62.0 we got No broken requirements found.

  5. Confirm the version in the running service

    Run python -c "import pyasn1; print(pyasn1.__version__)" inside the container or server. Expect 0.6.5.

  6. Run your audit again

    Run pip-audit or your scanner of choice. Expect No known vulnerabilities found for pyasn1.

We also checked that normal use did not change. A DER round trip of a small record with an INTEGER, a UTF8String, an OCTET STRING and a BIT STRING produced identical bytes on 0.6.4 and 0.6.5. A REAL value also decoded back to itself on both.

One documented behaviour change exists. The 0.6.5 release notes say nested constructed OCTET STRING fragments now contribute their decoded octets. Valid DER never splits strings into fragments, so certificates and keys decode exactly as before; only unusual BER input can differ.

What changed in the pyasn1 0.6.5 source code?

Only one library file changed: pyasn1/codec/ber/decoder.py, plus the version string. We downloaded both source releases from PyPI with pip download and diffed them; the rest of the diff is new tests.

You can confirm the fix is installed by searching the decoder for the new lines. Run this inside the environment you upgraded.

bash
grep -n "len(componentTypes) < 2\|int.from_bytes(bytes(chunk), 'big')\|b''.join(fragments)" "$(python -c 'import pyasn1.codec.ber.decoder as d; print(d.__file__)')"
# 0.6.5 prints five matching lines; 0.6.4 prints none

On our 0.6.5 install that grep found the join in three places, the int.from_bytes call for REAL and the two-tag check for schemaless containers. BIT STRING has a new helper method, _appendFragment.

Limits the decoder already has, and the one it does not

The 0.6.5 decoder enforces a nesting depth of 100 levels and a long tag ID limit of 20 octets. Both are module constants in decoder.py, named MAX_NESTING_DEPTH and MAX_TAG_OCTETS, not options you pass to decode().

There is no setting for total input size or number of fragments. Treat these constants as undocumented internals as of 1 November 2026, and pin your version if you rely on them.

How do you protect any parser from inputs like these?

Cap the input size before parsing, and run parsing where you can really stop it. Upgrading fixes these four bugs; limits protect you from the next one.

Cap the size first

Most inputs have a natural maximum. A single certificate is usually a few kilobytes, so the upload tool can reject anything far larger before calling decode(). Pick the cap from your largest real file, with headroom.

Quadratic work needs a large input to hurt, but not a huge one. The REAL advisory's local reproduction on v0.6.4 measured BER at 0.209 s, 0.884 s, and 3.451 s for 50,000, 100,000, and 200,000 mantissa octets.

So pick the smallest cap your real files allow; for certificates, a few kilobytes plus headroom. The same advisory puts it this way: "The appropriate size limit depends on the application's available CPU time and request volume."

The BIT STRING advisory lists this as a workaround and says plainly that it limits exposure but does not fix the quadratic behaviour. A cap shrinks the worst case; it does not remove it.

Know which timeouts actually stop work

An asyncio timeout cannot interrupt pure-Python work that never awaits. We wrapped a CPU-bound function in asyncio.wait_for with a 0.1 second timeout.

python
# Generic demo: an asyncio timeout cannot interrupt CPU-bound code that never awaits.
import asyncio, time

def slow_parse():
    # stands in for any long pure-Python computation
    total = 0
    for i in range(30_000_000):
        total += i
    return total

async def handler():
    return slow_parse()          # no await inside: the loop is stuck here

async def main():
    t0 = time.perf_counter()
    try:
        await asyncio.wait_for(handler(), timeout=0.1)
        print("finished, no timeout raised")
    except asyncio.TimeoutError:
        print("TimeoutError raised")
    print(f"elapsed: {time.perf_counter() - t0:.2f} s (timeout was 0.10 s)")

asyncio.run(main())

It printed finished, no timeout raised and an elapsed time of 0.97 s on CPython 3.13.18. The timeout only fires when the coroutine yields, and a busy decoder never yields. Run risky parsing in a separate process you can kill, or keep inputs small enough that the worst case is short.

Prefer a schema and the strictest decoder you can

Passing asn1Spec avoids the schemaless guessing path, the only path the container advisory affects. Using the DER decoder where the protocol is DER rejects many odd encodings, but the advisory table shows it narrows these paths rather than closing them.

How do you explain the pyasn1 vulnerability in a system design interview?

Use it as an example of algorithmic-complexity denial of service: an input that is small to send but expensive to process. Interviewers want to hear where you put limits.

  1. Name the bug class

    A parser did quadratic work on crafted input, so a small message cost seconds of CPU. Availability was hit; the advisories describe no data exposure.

  2. Show the mechanism

    Growing a value by copying it on every fragment gives 1 + 2 + ... + N work. Collecting pieces and joining once makes it linear.

  3. Find the exposure

    List every place untrusted bytes reach a parser, including transitive dependencies like pyasn1 under pysnmp or ldap3.

  4. Layer the defences

    Patch the library, cap input size at the edge, and run parsing in a worker with a hard deadline it cannot ignore.

  5. Make it observable

    Track parse time per request and alert on outliers, so the next slow path shows up as a metric, not an outage.

Questions people ask

Can the pyasn1 vulnerability leak data or run code?
No, not according to the advisories. All four rate confidentiality and integrity impact as None and availability as High. The demonstrated harm is CPU exhaustion that makes a service slow or unresponsive. That still matters: monitoring that stops processing alerts, or a login service that stalls, is a real outage.
Is there a fix for older pyasn1 lines such as 0.4.x?
No, not as of 1 November 2026. The only patched release is 0.6.5. pyasn1 0.6.5 supports Python 3.8 and newer, so most environments can move straight to it. If a package pins pyasn1 below 0.6, upgrade or replace that package first.
I upgraded google-auth. Does that fix pyasn1?
Not by itself. google-auth 2.62.0 depends on pyasn1-modules, which accepts any pyasn1 from 0.6.1 up to, but not including, 0.7.0. An older pyasn1 still satisfies that range. Upgrade pyasn1 directly and check the version with pip show pyasn1.
Why does my scanner show a CVE when the advisory says No known CVE?
Because different databases record it differently. The maintainer advisory pages list no CVE, while PyPI's vulnerability data lists CVE-2026-119194 to CVE-2026-119197 as aliases of the four GHSA IDs. Both names point to the same bugs and the same fix, 0.6.5.
Are these the same bugs as the July 2026 pyasn1 advisories?
No. July's release 0.6.4 fixed three other issues: OBJECT IDENTIFIER decoding, unbounded long tag IDs and slow REAL to float conversion. The October advisories cover different loops in the same decoder file. If you upgraded to 0.6.4 in July, you still need 0.6.5.
Should I write a test that sends a malicious input to check my fix?
No, and you do not need one. Check the installed version, run pip-audit and grep the decoder for the new lines shown in this guide. The upstream project added regression tests for these bugs in its own test suite, so the behaviour is already covered there.

Sources and further reading

Practise this with Lala

Practise