Skip to content

Patch the Planet: A combined certificate and private-key PEM exposes the TLS server key before authentication

Low
gpotter2 published GHSA-p899-23mj-p25x Sep 8, 2026

Package

pip scapy (pip)

Affected versions

<2.8.0

Patched versions

2.8.0

Description

What happens

If a Scapy TLS server's certificate file also contains its private key — a common way to store the
two together — the server sends the private key to anyone who connects to it.

An ordinary 50-byte hello was enough. The server answered with a 2,146-byte certificate message
that had the complete private key inside it. The same server with the key kept in a separate file
sent 930 bytes and no key. Nothing about the connection has to be unusual, and this happens before
the client has proved anything about itself.

Background — what this code does

Scapy's Cert class loads X.509 certificates from DER or PEM files for TLS and other protocols.
Its
shared PEM loader supports files containing more than one PEM object by concatenating every
decoded
object at
scapy/layers/tls/cert.py:243-280.

TLSServerAutomaton is Scapy's active TLS server. It stores the configured certificate in the TLS
session at
scapy/layers/tls/automaton_srv.py:286-300
and sends that object in the server Certificate message at
scapy/layers/tls/automaton_srv.py:393-398.

How the code is reached

An application starts TLSServerAutomaton with mycert and mykey filenames. Combined PEM files
containing a certificate followed by its private key are a conventional certificate-deployment
format. The attacker only needs to connect and send a TLS 1.2 ClientHello that selects a
certificate-based cipher suite; the Certificate message is sent before client authentication.

The reproduction uses the standard TLS server automaton, its existing test certificate and key,
and default global Scapy configuration. It does not change conf.* or any Scapy cache file.

Why it matters

Every client that connects gets the key, and it is the whole key. Whoever holds it can impersonate
the service anywhere that certificate is trusted, and can decrypt any recorded session that used
RSA key exchange with it. The reproducer shows the key being handed over; it does not go on to do
either of those things.

The demonstrated disclosure is limited to additional valid PEM objects in the configured
certificate file. No claim is made that arbitrary trailing non-PEM bytes are emitted.

Reproduce it

Save the supplied script as reproduce.py in the Scapy checkout and run:

./.venv/bin/python reproduce.py

Expected result on commit 1f870205baae8baf1718bc20700d0d9ffc0e4324:

mixed PEM: boundary_held=False certificate_entries=1 entry_bytes=2146 private_key_der_bytes=1216 private_key_der_occurrences=1
certificate-only PEM: boundary_held=True certificate_entries=1 entry_bytes=930 private_key_der_occurrences=0

The two exchanges send equivalent ClientHello records to servers using the same certificate and
key. Only the mycert file differs: the first appends the valid private-key PEM, while the
control
contains the certificate alone. With the patch applied, both cases produce a 930-byte certificate
entry with zero private-key occurrences.

This was reproduced with Scapy 2.7.1rc1.post100, Python 3.13.14, default global configuration,
and conf.debug_dissector = 0.

Where it goes wrong

_PKIObjMaker.__call__() at
scapy/layers/tls/cert.py:268

decodes and concatenates every PEM object:

if b"-----BEGIN" in _raw:
    frmt = "PEM"
    pem = _raw
    der_list = split_pem(pem)
    der = b"".join(map(pem2der, der_list))

That behavior is useful for callers that expect multiple objects, but Cert represents one
certificate. _CertMaker.__call__() at
scapy/layers/tls/cert.py:911

parses the concatenation as one X509_Cert and retains any remaining bytes as that packet's
payload:

obj = _PKIObjMaker.__call__(cls, cert_path, _MAX_CERT_SIZE, "CERTIFICATE")
obj.__class__ = Cert
obj.marker = "CERTIFICATE"
try:
    cert = X509_Cert(obj._der)
except Exception:
    if conf.debug_dissector:
        raise
    raise Exception("Unable to import certificate")
obj.import_from_asn1pkt(cert)

The der property serializes the whole parsed packet at
scapy/layers/tls/cert.py:1127-1133.
The retained private-key payload therefore becomes part of the TLS certificate entry.

Suggested fix

         try:
             cert = X509_Cert(obj._der)
         except Exception:
             if conf.debug_dissector:
                 raise
             raise Exception("Unable to import certificate")
+        cert.remove_payload()
         obj.import_from_asn1pkt(cert)

Cert is a singular certificate wrapper, so it should discard bytes after the first parsed X.509
object before storing it. This preserves certificate-only DER and PEM behavior while ensuring that
additional PEM objects are not returned by Cert.der.

The regression test is in test/scapy/layers/tls/cert.uts. Adding only the test to the affected
source makes it fail; restoring the source fix makes it pass. The complete certificate suite
passes 69 of 69 tests with the patch.

Performance impact of the fix, measured on one computer by running the same test before and after:
discarding the payload took 608.5 ns before and 526 ns after, 13.6% faster — a difference of 82
ns. Repeat runs of that test moved by about 8%, so that difference is larger than the test's own
variation.

Affected

  • Package: Scapy · Branch: master
  • Confirmed on: commit
    1f870205baae8baf1718bc20700d0d9ffc0e4324,
    version 2.7.1rc1.post100
  • Severity: High — CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N (7.5). The score reflects
    unauthenticated remote disclosure of the server's complete private key, without a separately
    demonstrated integrity or availability effect.
  • CWE: CWE-200 — Exposure of Sensitive Information to an Unauthorized Actor

Credit

Reported by: Clinton Thomas (@KernelClint) of Trail of Bits, in collaboration with OpenAI.
Found with GPT-5.6-Cyber as part of the Patch the Planet security initiative.

Severity

Low

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Local
Attack complexity
Low
Privileges required
Low
User interaction
None
Scope
Unchanged
Confidentiality
Low
Integrity
None
Availability
None

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N

CVE ID

No known CVE

Weaknesses

Exposure of Sensitive Information to an Unauthorized Actor

The product exposes sensitive information to an actor that is not explicitly authorized to have access to that information. Learn more on MITRE.

Credits