<?xml version='1.0' encoding='utf-8'?>
<?xml-stylesheet type="text/xsl" href="/sheet.xsl"?><rss version="2.0"><channel><title>ENOSUCHBLOG</title><description>Programming, philosophy, pedaling.</description><item><title>Registering my dissatisfaction with GitHub</title><link>https://blog.yossarian.net/2026/04/29/Registering-my-dissatisfaction-with-GitHub</link><description>Mini-post.</description><ns0:encoded xmlns:ns0="http://purl.org/rss/1.0/modules/content/">&lt;body morss_own_score="2.5356037151702786" morss_score="36.02874435365173"&gt;
&lt;h1&gt;ENOSUCHBLOG&lt;/h1&gt;
&lt;h2&gt;&lt;em&gt;Programming, philosophy, pedaling.&lt;/em&gt;&lt;/h2&gt;
&lt;hr&gt;
&lt;h1&gt;
&lt;a href="https://blog.yossarian.net/2026/04/29/Registering-my-dissatisfaction-with-GitHub"&gt;Registering my dissatisfaction with GitHub&lt;/a&gt;
&lt;/h1&gt;
&lt;p&gt;
&lt;em&gt;Apr 29, 2026&lt;/em&gt;

       

    
      &lt;span&gt;
        Tags:
        
        
          &lt;a href="https://blog.yossarian.net/tags#oss"&gt;oss&lt;/a&gt;
&lt;/span&gt;
    

       

    
  &lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Mini-post.&lt;/p&gt;
&lt;p&gt;I read &lt;a href="https://mitchellh.com/writing/ghostty-leaving-github"&gt;Mitchell Hashimoto’s blog post&lt;/a&gt;
yesterday and it resonated with me; this part in particular:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Since then, I’ve opened GitHub every single day. Every day, multiple times per day,
for over 18 years. Over half my life. A handful of exceptions in there (I’d love to see the
data), but I can’t imagine more than a week per year.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I can’t claim quite the same seniority (I
&lt;a href="https://github.com/woodruffw"&gt;joined&lt;/a&gt; in 2012; my user ID is &lt;code&gt;3059210&lt;/code&gt;),
but I think I can fairly say that GitHub is both my favorite and the single most
important service I’ve interacted with, both as a hobbyist maintainer and as a professional
software engineer. I wouldn’t have the friends, connections, career, &amp;amp;c. that
I have today if it wasn’t for GitHub.&lt;/p&gt;
&lt;p&gt;Despite that, using GitHub has become a thoroughly &lt;em&gt;dissatisfying&lt;/em&gt; experience
over the last 18 months. I don’t know exactly &lt;em&gt;why&lt;/em&gt; that is&lt;sup&gt;&lt;a href="https://blog.yossarian.net/2026/04/29/Registering-my-dissatisfaction-with-GitHub#fn:theories"&gt;1&lt;/a&gt;&lt;/sup&gt;,
but that is the brute reality. I now frequently interact with GitHub with a defensive,
rather than optimistic posture: I &lt;em&gt;assume&lt;/em&gt; that things will be slow, buggy,
or just plain broken, and my disappointment with the state of affairs
has been ground down from surprise (and a desire to help by reporting bugs)
to resignation and apathy.&lt;/p&gt;
&lt;p&gt;I hope that GitHub can turn things around, and that I get to use it for
years to come. But as of now, I’m dissatisfied with it and, for the first time
in over a decade, I am &lt;em&gt;seriously&lt;/em&gt; considering alternatives.&lt;/p&gt;
&lt;hr&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Theories abound, and are too well-trodden to be worth repeating in detail.
         In brief: a loss of pride-in-craft (stemming from Microsoft’s acquisition),
         deprioritization of feature development in the core product, an ill-advised React
         frontend rewrite, talent loss (voluntary and involuntary), a surge of AI-driven traffic,
         &amp;amp;c, &amp;amp;c. I have no idea which (if any) of these are to blame. &lt;a href="https://blog.yossarian.net/2026/04/29/Registering-my-dissatisfaction-with-GitHub#fnref:theories"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;hr&gt;

&lt;a href="https://blog.yossarian.net/2026/04/11/Brocards-for-vulnerability-triage"&gt;Previously&lt;/a&gt;

&lt;/body&gt;
</ns0:encoded><pubDate>Wed, 29 Apr 2026 00:00:00 UTC</pubDate></item><item><title>Brocards for vulnerability triage</title><link>https://blog.yossarian.net/2026/04/11/Brocards-for-vulnerability-triage</link><description>I spend some of my hobby time doing vulnerability triage on open source projects. As part of that, I see (and filter through) a lot of nonsense1. Spam, “beg bounty” submissions, and increasingly zero-effort LLM submissions. &amp;#8617;</description><ns0:encoded xmlns:ns0="http://purl.org/rss/1.0/modules/content/">&lt;body morss_own_score="2.667452830188679" morss_score="100.56608058906077"&gt;
&lt;h1&gt;ENOSUCHBLOG&lt;/h1&gt;
&lt;h2&gt;&lt;em&gt;Programming, philosophy, pedaling.&lt;/em&gt;&lt;/h2&gt;
&lt;hr&gt;
&lt;h1&gt;
&lt;a href="https://blog.yossarian.net/2026/04/11/Brocards-for-vulnerability-triage"&gt;Brocards for vulnerability triage&lt;/a&gt;
&lt;/h1&gt;
&lt;p&gt;
&lt;em&gt;Apr 11, 2026&lt;/em&gt;

       

    
      &lt;span&gt;
        Tags:
        
        
          &lt;a href="https://blog.yossarian.net/tags#oss"&gt;oss&lt;/a&gt;,
        
          &lt;a href="https://blog.yossarian.net/tags#security"&gt;security&lt;/a&gt;
&lt;/span&gt;
    

       

    
  &lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;I spend some of my hobby time doing vulnerability triage on open source projects.
As part of that, I see (and filter through) a lot of nonsense&lt;sup&gt;&lt;a href="https://blog.yossarian.net/2026/04/11/Brocards-for-vulnerability-triage#fn:nonsense"&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;Nonsense is not unique to vulnerability triage: lawyers deal with it
too. To cope with it in the legal world, they use &lt;a href="https://en.wikipedia.org/wiki/Brocard_(law)"&gt;brocards&lt;/a&gt; —
concise aphorisms that capture the essence of a legal principle. Any given
brocard is not &lt;em&gt;universally&lt;/em&gt; true&lt;sup&gt;&lt;a href="https://blog.yossarian.net/2026/04/11/Brocards-for-vulnerability-triage#fn:law"&gt;2&lt;/a&gt;&lt;/sup&gt;, but provides a standard by which a claim
can quickly be evaluated for legitimacy.&lt;/p&gt;
&lt;p&gt;Vulnerability triage has its own brocards, but I couldn’t find a comprehensive
list of them anywhere. This is my attempt to compile such a list.&lt;/p&gt;
&lt;h2&gt;No vulnerability report without a threat model&lt;a href="https://blog.yossarian.net/2026/04/11/Brocards-for-vulnerability-triage#no-vuln-without-threat-model"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://alexgaynor.net"&gt;Alex Gaynor&lt;/a&gt; explains this one well in
&lt;a href="https://alexgaynor.net/2025/oct/20/motion-to-dismiss/"&gt;Motion to Dismiss for Failure to State a Vulnerability&lt;/a&gt;: a vulnerability report
can be safely dismissed if it lacks a threat model, or if the threat model
presented is incoherent.&lt;/p&gt;
&lt;p&gt;Examples include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;A report for a Python API that raises an exception in some undocumented
or surprising cases&lt;sup&gt;&lt;a href="https://blog.yossarian.net/2026/04/11/Brocards-for-vulnerability-triage#fn:exceptions"&gt;3&lt;/a&gt;&lt;/sup&gt;, but doesn’t explain how an attacker could exploit that
behavior to cause harm.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;A report for a hang or stall in a local developer tool. Hangs are undesirable behavior,
but the opportunity for harm from one is negligible in a developer tooling context:
the developer can always just kill the process.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;No exploit from the heavens&lt;a href="https://blog.yossarian.net/2026/04/11/Brocards-for-vulnerability-triage#no-exploit-from-heavens"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Closely related are vulnerability reports that describe a severe end state,
but only under attacker capability assumptions that are
&lt;em&gt;more powerful than the vulnerability itself&lt;/em&gt;. In effect, in order to mount
an attack that exploits the vulnerability, the attacker would &lt;em&gt;already&lt;/em&gt;
need to have an equal or more powerful capability than the vulnerability itself
provides.&lt;/p&gt;
&lt;p&gt;Examples include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;A report for content manipulation on a web service, where the manipulation
can only occur if the attacker is an active &lt;a href="https://en.wikipedia.org/wiki/Man-in-the-middle_attack"&gt;meddler in the middle&lt;/a&gt;.
No vulnerability exists here, because an active MiTM could send entirely
arbitrary content, and would not have to limit themselves to manipulating
pre-existing content.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;A report for code execution via memory corruption in CPython, where the
memory corruption occurs by directly manipulating CPython’s object internals
at runtime (via ctypes, for example). No vulnerability exists here, because
the attacker is &lt;em&gt;already&lt;/em&gt; running arbitrary code to perform the corruption.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href="https://devblogs.microsoft.com/oldnewthing/20060508-22/?p=31283"&gt;Raymond Chen has 2006 post that covers the same concept&lt;/a&gt;. Thanks to
&lt;a href="https://ldpreload.com"&gt;Geoffrey Thomas&lt;/a&gt; for sharing that with me!&lt;/p&gt;
&lt;h2&gt;No vulnerability outside of usage&lt;a href="https://blog.yossarian.net/2026/04/11/Brocards-for-vulnerability-triage#no-vuln-outside-usage"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;A vulnerability report can be safely dismissed if it describes a behavior
that &lt;em&gt;could&lt;/em&gt; occur, but does not in fact occur in actual usage of the software.&lt;/p&gt;
&lt;p&gt;Examples include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;A report for a vulnerability in a private API within a library,
where the only (private) usage of that API is not vulnerable.&lt;/p&gt;
&lt;p&gt;For example, a C codebase might have a function that takes a &lt;code&gt;char *&lt;/code&gt; and
exhibits a buffer overflow with strings longer than 100 bytes,
but a codebase where all calls to that function are statically assertable
to not exceed that size is not vulnerable.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Similarly, a report for a vulnerability in an API (public &lt;em&gt;or&lt;/em&gt; private),
where the vulnerability can only occur by violating an invariant that
the programmer is responsible for maintaining.&lt;/p&gt;
&lt;p&gt;For example, an API might have a precondition that an input string is valid
UTF-8, and an input that violates this precondition may cause an uncontrolled
program abort. However, a fuzzer that discovers this behavior has not
found a vulnerability, because in a real program the programmer is responsible
for ensuring that the “building blocks” of the API are composed together.&lt;/p&gt;
&lt;p&gt;It’s worth noting some nuance here: because the programmer is responsible for
maintaining the invariant, there &lt;em&gt;is&lt;/em&gt; a potentially legitimate vulnerability
when &lt;em&gt;usage&lt;/em&gt; of the API violates the invariant. By analogy:
&lt;code&gt;free(3)&lt;/code&gt; is not considered vulnerable to a double free, but a program that calls &lt;code&gt;free(3)&lt;/code&gt; on
an already freed pointer is considered vulnerable to a double free.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;A re-report of a upstream’s vulnerability, where the upstream’s’s vulnerable
behavior is not reachable in the downstream.&lt;/p&gt;
&lt;p&gt;For example, CPython is shipped with a build of OpenSSL, and OpenSSL
&lt;a href="https://www.openssl.org/news/vulnerabilities.html"&gt;regularly has security advisories&lt;/a&gt;.
However, CPython’s exposure to OpenSSL is mostly limited to SSL/TLS and a subset
of the X.509 APIs, and therefore vulnerabilities outside of these surfaces
do not constitute a reasonable re-report to CPython.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;No vulnerability from standard behavior&lt;a href="https://blog.yossarian.net/2026/04/11/Brocards-for-vulnerability-triage#no-vuln-from-standard-behavior"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Perhaps my most controversial (and personal) brocard: a vulnerability report can
be safely dismissed if the behavior described is a direct consequence of the
software’s &lt;em&gt;correct&lt;/em&gt; adherence to a standard or specification. In these instances
the vulnerability (if one exists) is present within the standard itself,
and not the implementation.&lt;/p&gt;
&lt;p&gt;Examples include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Behavior stemming from “robustness” requirements in standards. Many RFCs
and similar standards inadvisably follow the (poorly named)
&lt;a href="https://en.wikipedia.org/wiki/Robustness_principle"&gt;robustness principle&lt;/a&gt;, and allow interactions that are not well-defined
(often by allowing the implementer to make a judgement call about
the &lt;em&gt;intended&lt;/em&gt; semantics of the interaction). For example, &lt;a href="https://datatracker.ietf.org/doc/html/rfc7230"&gt;RFC 7230&lt;/a&gt;
has this under “Message Parsing Robustness” (ss. 3.5):&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;In the interest of robustness, a server that is expecting to receive
 and parse a request-line SHOULD ignore at least one empty line (CRLF)
 received prior to the request-line.&lt;/p&gt;
&lt;p&gt;Although the line terminator for the start-line and header fields is
 the sequence CRLF, a recipient MAY recognize a single LF as a line
 terminator and ignore any preceding CR.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Behavior stemming from cryptographic requirements that are insecure in
isolation but secure by construction. The “classic”
example of this is (typically automated) reports of MD5 usage, where that
usage is solely in constructions where MD5 is not actually broken
(i.e. HMAC-MD5). There’s a strong argument to be made that a &lt;em&gt;better&lt;/em&gt;
hash function should be used where permitted, but the presence of MD5
in an HMAC construction is not itself a vulnerability.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The nuance with this is that an implementation that &lt;em&gt;chooses&lt;/em&gt; to be more
strict than the standard requires &lt;em&gt;should&lt;/em&gt; be considered vulnerable if the
intended strictness is violated.&lt;/p&gt;
&lt;h2&gt;No vulnerability from documented behavior&lt;a href="https://blog.yossarian.net/2026/04/11/Brocards-for-vulnerability-triage#no-vuln-from-documented-behavior"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;(Many thanks to &lt;a href="https://github.com/hugovk"&gt;Hugo van Kemenade&lt;/a&gt; for this one!)&lt;/p&gt;
&lt;p&gt;Similar to the above: a vulnerability report can be safely dismissed if the behavior
is documented to occur, &lt;em&gt;particularly&lt;/em&gt; when the documentation explicitly
describes the security implications of the behavior or specific
contexts in which the software is unsafe to use.&lt;/p&gt;
&lt;p&gt;Examples include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Python’s &lt;a href="https://docs.python.org/3/library/http.server.html"&gt;&lt;code&gt;http.server&lt;/code&gt;&lt;/a&gt; is explicitly documented as not suitable
for production use, as it only implements &lt;a href="https://docs.python.org/3/library/http.server.html#http-server-security"&gt;basic security checks&lt;/a&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Python’s &lt;a href="https://docs.python.org/3/library/pickle.html"&gt;&lt;code&gt;pickle&lt;/code&gt;&lt;/a&gt; is explicitly documented as not secure, full stop.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Like with the previous brocard, the nuance here is that a downstream &lt;em&gt;usage&lt;/em&gt;
that violates the documented guidelines for use may be considered vulnerable.
In other words: a report against &lt;code&gt;pickle&lt;/code&gt; itself (for e.g. enabling code execution)
may be safely dismissed, but a report against a downstream usage of &lt;code&gt;pickle&lt;/code&gt;
that ignores the documented warnings may be considered valid.&lt;/p&gt;
&lt;h2&gt;No cure worse than the disease&lt;a href="https://blog.yossarian.net/2026/04/11/Brocards-for-vulnerability-triage#no-cure-worse-than-disease"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The maintainer should reject (or contest) vulnerability reports whose consequences are
worse than the consequences of the vulnerability itself.&lt;/p&gt;
&lt;p&gt;The classic example of this is &lt;a href="https://blog.yossarian.net/2022/12/28/ReDoS-vulnerabilities-and-misaligned-incentives"&gt;ReDoS&lt;/a&gt; “vulnerabilities,” particularly in contexts
where the impact of the “denial of service” is negligible&lt;sup&gt;&lt;a href="https://blog.yossarian.net/2026/04/11/Brocards-for-vulnerability-triage#fn:negligible"&gt;4&lt;/a&gt;&lt;/sup&gt;. These
reports typically involve nontrivial amounts of maintainer time and effort to
triage, followed by nontrivial amounts of &lt;em&gt;downstream&lt;/em&gt; time and effort to
remediate, effectively resulting in a denial of service on the community itself.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/advisories/GHSA-5239-wwwm-4pmq"&gt;CVE-2026-4539&lt;/a&gt; is a recent case of this: an anonymous reporter filed a CVE
against &lt;a href="https://github.com/pygments/pygments"&gt;pygments&lt;/a&gt; with &lt;a href="https://vuldb.com/"&gt;VulDB&lt;/a&gt;, seemingly bypassing any maintainer or community review.
This report was not accompanied by a fixed version (because it’s junk, and
ignores &lt;a href="https://pygments.org/docs/security/"&gt;pygments’ own security policy&lt;/a&gt;),
but lit up tens of thousands of downstream dependencies with a “medium”
severity vulnerability, &lt;a href="https://github.com/pygments/pygments/issues/3058"&gt;causing significant disruption&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The status quo in 2026 is that the CVE ecosystem unreasonably places
the onus on maintainers to contest this kind of spam when adversarial reporters
bypass them entirely.&lt;/p&gt;
&lt;h2&gt;The report is neither necessary nor sufficient&lt;a href="https://blog.yossarian.net/2026/04/11/Brocards-for-vulnerability-triage#report-neither-necessary-nor-sufficient"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The presence of a vulnerability report (and a CVE or other identifier for
that report) is neither necessary nor sufficient for a vulnerability to exist.&lt;/p&gt;
&lt;p&gt;This cuts both ways: many (perhaps the majority of) vulnerabilities are never
“formally” reported, and many formal reports do not actually describe
meaningful vulnerabilities (per above). Consequently, no &lt;em&gt;unvalidated&lt;/em&gt; assumption should
ever be made about the relationship between the presence of a report and the
presence of a vulnerability.&lt;/p&gt;
&lt;p&gt;This is another unfortunate status quo, one that stems from (seemingly
intentional) &lt;a href="https://blog.yossarian.net/2024/03/20/More-thoughts-on-vulnerabilities-and-misaligned-incentives"&gt;strategic ambiguity&lt;/a&gt; in the vulnerability reporting ecosystem:
partners like MITRE benefit simultaneously from being &lt;em&gt;perceived&lt;/em&gt; as a high-quality
source of vulnerability information, while also being able to disclaim any responsibility
for communicating anything other than a stable identifier for a &lt;em&gt;claim&lt;/em&gt; of vulnerability.&lt;/p&gt;
&lt;hr&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Spam, “beg bounty” submissions, and increasingly zero-effort LLM submissions. &lt;a href="https://blog.yossarian.net/2026/04/11/Brocards-for-vulnerability-triage#fnref:nonsense"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Brocards are also not &lt;em&gt;themselves&lt;/em&gt; law. &lt;a href="https://blog.yossarian.net/2026/04/11/Brocards-for-vulnerability-triage#fnref:law"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Vulnerability reports for libraries/components that raise exceptions
           in managed libraries are very common, and often belie a fundamental
           misunderstanding of the language’s execution contract. In Python,
           for example, the presumed execution contract is that an exception
           can occur at any point, and that a system should always be prepared
           to handle an exception. This doesn’t mean that every single block
           of code needs a &lt;code&gt;try..except&lt;/code&gt;, but that all exceptions &lt;em&gt;do&lt;/em&gt;
           get handled eventually, and therefore vulnerabilities only
           emerge when an exception is &lt;em&gt;mishandled&lt;/em&gt;. &lt;a href="https://blog.yossarian.net/2026/04/11/Brocards-for-vulnerability-triage#fnref:exceptions"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Two common reasons for the negligibility of ReDoS: (1) the
           only impact is to local workflows where the “denial” is trivially
           cancellable (such as a developer tool running in a terminal),
           and (2) the vulnerable behavior is only exhibited on arbitrary
           inputs that would otherwise pose a denial of service “risk”. &lt;a href="https://blog.yossarian.net/2026/04/11/Brocards-for-vulnerability-triage#fnref:negligible"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;hr&gt;
&lt;span&gt;
  Discussions:
  
  &lt;a href="https://www.reddit.com/r/enosuchblog/comments/1sir2mf/brocards_for_vulnerability_triage/"&gt;Reddit&lt;/a&gt;
&lt;a href="https://infosec.exchange/@yossarian/116387519824893265"&gt;Mastodon&lt;/a&gt;
&lt;a href="https://bsky.app/profile/yossarian.net/post/3mjai6eav4527"&gt;Bluesky&lt;/a&gt;
&lt;/span&gt;
&lt;hr&gt;

&lt;a href="https://blog.yossarian.net/2025/12/29/Some-flexibility-with-Go-s-sumdb"&gt;Previously&lt;/a&gt;


&lt;a href="https://blog.yossarian.net/2026/04/29/Registering-my-dissatisfaction-with-GitHub"&gt;Newer&lt;/a&gt;

&lt;/body&gt;
</ns0:encoded><pubDate>Sat, 11 Apr 2026 00:00:00 UTC</pubDate></item><item><title>Some flexibility with Go’s sumdb</title><link>https://blog.yossarian.net/2025/12/29/Some-flexibility-with-Go-s-sumdb</link><description>I noticed this a year or two ago, but forgot to write it up back then.</description><ns0:encoded xmlns:ns0="http://purl.org/rss/1.0/modules/content/">&lt;body morss_own_score="2.794912559618442" morss_score="131.56319682498543"&gt;
&lt;h1&gt;ENOSUCHBLOG&lt;/h1&gt;
&lt;h2&gt;&lt;em&gt;Programming, philosophy, pedaling.&lt;/em&gt;&lt;/h2&gt;
&lt;hr&gt;
&lt;h1&gt;
&lt;a href="https://blog.yossarian.net/2025/12/29/Some-flexibility-with-Go-s-sumdb"&gt;Some flexibility with Go's sumdb&lt;/a&gt;
&lt;/h1&gt;
&lt;p&gt;
&lt;em&gt;Dec 29, 2025&lt;/em&gt;

       

    
      &lt;span&gt;
        Tags:
        
        
          &lt;a href="https://blog.yossarian.net/tags#cryptography"&gt;cryptography&lt;/a&gt;,
        
          &lt;a href="https://blog.yossarian.net/tags#go"&gt;go&lt;/a&gt;,
        
          &lt;a href="https://blog.yossarian.net/tags#security"&gt;security&lt;/a&gt;
&lt;/span&gt;
    

       

    
  &lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;I noticed this a year or two ago, but forgot to write it up back then.&lt;/p&gt;
&lt;p&gt;I don’t think it’s particularly serious or important, but it’s an interesting
demonstration of how flexibility in identity can make
monitoring in transparency schemes nontrivial, as well as conflict with
user intuitions around which identities are equivalent.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: Go’s sumdb log entries are unique per module version, but there
are typically multiple valid case forms for the module &lt;em&gt;path&lt;/em&gt; that point to the
same module version &lt;em&gt;contents&lt;/em&gt;. These forms are &lt;strong&gt;not confusable&lt;/strong&gt; at import time,
but they complicate the monitoring story and could form the basis of a
typosquatting-like attack. Because the “typo” is just a case variation, it
looks less suspicious than a normal typo.&lt;/p&gt;
&lt;h2&gt;Background&lt;a href="https://blog.yossarian.net/2025/12/29/Some-flexibility-with-Go-s-sumdb#background"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Go’s packaging design includes the &lt;a href="https://proxy.golang.org"&gt;Go module proxy&lt;/a&gt;, which is effectively
a caching proxy for Go modules. This serves (at least) two main purposes:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;It makes Go packaging more reliable and resilient: &lt;code&gt;src.example.com/some/module@v1.2.3&lt;/code&gt;
will continue to work even if &lt;code&gt;src.example.com&lt;/code&gt; goes down, so long
as &lt;em&gt;someone&lt;/em&gt; has previously resolved that module and version through the proxy.
This also has the knock-on effect of making Go’s module resolution fast.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;It provides a degree of integrity against unreliable, malicious, or compromised&lt;sup&gt;&lt;a href="https://blog.yossarian.net/2025/12/29/Some-flexibility-with-Go-s-sumdb#fn:tag"&gt;1&lt;/a&gt;&lt;/sup&gt;
upstreams: the proxy’s copy of the module is can’t be mutated &lt;em&gt;post facto&lt;/em&gt;
by the origin, meaning that downstreams that haven’t previously locked
their resolution still receive a version that’s consistent.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This second property is really useful, but still requires a degree of trust:
if the proxy &lt;em&gt;itself&lt;/em&gt; were compromised, it could serve malicious modules
to not-yet-locked downstreams. This would be easy for an attacker
to do in a precise manner (i.e. target specific victims) as well as difficult
to detect, since independent downstrams have no easy way of gossiping
amongst themselves about the proxy’s claimed responses.&lt;/p&gt;
&lt;h2&gt;Transparency&lt;a href="https://blog.yossarian.net/2025/12/29/Some-flexibility-with-Go-s-sumdb#transparency"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;In most ecosystems, the situation above (where the index is &lt;em&gt;de jure&lt;/em&gt;
immutable, but not easily &lt;em&gt;verifiably&lt;/em&gt; so) is the norm.&lt;/p&gt;
&lt;p&gt;However, Go went a step further and introduced the Go &lt;a href="https://go.googlesource.com/proposal/+/master/design/25530-sumdb.md"&gt;checksum database&lt;/a&gt;,
or “sumdb.” Go’s sumdb is a transparency log: an immutable, append-only
datastructure that yields &lt;em&gt;cryptographic proofs of inclusion&lt;/em&gt; for data entered
into it. Russ Cox has a nice &lt;a href="https://research.swtch.com/tlog"&gt;explainer on Merkle Trees and transparency log design&lt;/a&gt;;
it’s also the same basic technology behind &lt;a href="https://certificate.transparency.dev/"&gt;Certificate Transparency&lt;/a&gt; and
&lt;a href="https://www.sigstore.dev"&gt;Sigstore&lt;/a&gt;, albeit with different properties in each case&lt;sup&gt;&lt;a href="https://blog.yossarian.net/2025/12/29/Some-flexibility-with-Go-s-sumdb#fn:claimantmodel"&gt;2&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;With this database, Go’s module proxy becomes &lt;em&gt;verifiably monitorable and
auditable&lt;/em&gt;: the proxy is responsible for obtaining a &lt;em&gt;log entry&lt;/em&gt; prior to
serving a new module version, and clients can efficiently verify that the entry:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Exists in the log (i.e., the entry is not serving content that it hasn’t
committed to).&lt;/li&gt;
&lt;li&gt;Matches the claimed module content (i.e., the entry is not a
&lt;em&gt;valid but unrelated&lt;/em&gt; log entry).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In effect, this forces our would-be attacker into the open: they’re still
able to upload whatever malicious code they please&lt;sup&gt;&lt;a href="https://blog.yossarian.net/2025/12/29/Some-flexibility-with-Go-s-sumdb#fn:design"&gt;3&lt;/a&gt;&lt;/sup&gt;, but they must &lt;em&gt;commit&lt;/em&gt;
to doing so in a globally visible manner.&lt;/p&gt;
&lt;h2&gt;Monitoring&lt;a href="https://blog.yossarian.net/2025/12/29/Some-flexibility-with-Go-s-sumdb#monitoring"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Transparency &lt;em&gt;itself&lt;/em&gt; is a useful technique for the game-theoretic reason
mentioned above: many attackers want to operate surreptitously and without
public evidence, and transparency logs prevent that.&lt;/p&gt;
&lt;p&gt;However, the mere presence of a transparency log does not foreclose on all
possible risks: the log itself needs to be &lt;em&gt;monitored&lt;/em&gt; (and &lt;em&gt;audited&lt;/em&gt;&lt;sup&gt;&lt;a href="https://blog.yossarian.net/2025/12/29/Some-flexibility-with-Go-s-sumdb#fn:auditors"&gt;4&lt;/a&gt;&lt;/sup&gt;).&lt;/p&gt;
&lt;p&gt;For example, maintainers want to know if an unexpected release occurs
(indicating compromise) or if a release’s contents vary unexpectedly
(indicating host/proxy tampering). Similarly, downstreams may wish to monitor
their dependencies for unexpected activity.&lt;/p&gt;
&lt;p&gt;The transparency log operator controls submission to the log, and Go’s sumdb
is no exception. This allows the operator to decide what goes into the log:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Every module version exists exactly once in the log. In other words:
&lt;code&gt;src.example.com/foo/bar@v1.2.3&lt;/code&gt; &lt;em&gt;should&lt;/em&gt; have at most exactly one log entry.
Uniqueness&lt;sup&gt;&lt;a href="https://blog.yossarian.net/2025/12/29/Some-flexibility-with-Go-s-sumdb#fn:uniqueness"&gt;5&lt;/a&gt;&lt;/sup&gt; is not a native property of Merkle tree-based logs; it’s checked
by log auditors.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Only the Go proxy itself can submit entries to the log. External users
can &lt;em&gt;induce&lt;/em&gt; log entry creation by requesting module versions from the proxy,
but the proxy is responsible for constructing and submitting the entry
in accordance with the log’s policies.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Flexibility&lt;a href="https://blog.yossarian.net/2025/12/29/Some-flexibility-with-Go-s-sumdb#flexibility"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;So, where does that leave us?&lt;/p&gt;
&lt;p&gt;Recall that a module version is a pair of (module path, version).
&lt;a href="https://pkg.go.dev/golang.org/x/mod/module"&gt;Per &lt;code&gt;x/mod/module&lt;/code&gt;&lt;/a&gt;, the module path is the substring of a filesystem path
in the context of the download cache. However, in the context of the proxy
protocol, it’s a URL&lt;sup&gt;&lt;a href="https://blog.yossarian.net/2025/12/29/Some-flexibility-with-Go-s-sumdb#fn:https"&gt;6&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;In both of these contexts, Go correctly observes that case-sensitivity can’t
be assumed: &lt;code&gt;src.example.com/Foo/Bar&lt;/code&gt; and &lt;code&gt;src.example.com/foo/bar&lt;/code&gt; must
be treated as distinct module paths, even if a given filesystem or HTTP server
treats them as equivalent.&lt;/p&gt;
&lt;p&gt;To keep them distinct, Go uses a very small escaping scheme in
potentially-insensitive contexts: every uppercase letter
is replaced with its lowercase equivalent, prefixed by an exclamation mark (&lt;code&gt;!&lt;/code&gt;).
This is unambiguous because Go &lt;a href="https://go.dev/ref/mod#go-mod-file-ident"&gt;otherwise forbids&lt;/a&gt; &lt;code&gt;!&lt;/code&gt; in module paths.&lt;/p&gt;
&lt;p&gt;Using the example above, the module path &lt;code&gt;src.example.com/Foo/Bar&lt;/code&gt; becomes
&lt;code&gt;src.example.com/!foo/!bar&lt;/code&gt; in the proxy protocol and, by extension,
in the sumdb.&lt;/p&gt;
&lt;p&gt;The consequence of this is that there are &lt;em&gt;often&lt;/em&gt; multiple valid URL forms
that point to the same module version contents. That, in turn, means that
there are multiple valid log entries that correspond to the same module
version contents…almost.&lt;/p&gt;
&lt;p&gt;For example, here’s &lt;code&gt;github.com/google/uuid@v1.6.0&lt;/code&gt;
and &lt;code&gt;github.com/Google/uuid@v1.6.0&lt;/code&gt; pulled from the &lt;code&gt;/lookup&lt;/code&gt; API:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;pre&gt;1
2
3
4
5
6
7
8
9
10
&lt;/pre&gt;&lt;/td&gt;&lt;td&gt;&lt;pre&gt;% curl https://sum.golang.org/lookup/github.com/google/uuid@v1.6.0
22152757
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0&lt;span&gt;=&lt;/span&gt;
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo&lt;span&gt;=&lt;/span&gt;

go.sum database tree
48546416
cEcdZrD3Oio0/tZ9JP2gKMdqeMWAvwiLDx0G2r3MJGI&lt;span&gt;=&lt;/span&gt;

— sum.golang.org Az3grl3WPuhg73ePSVE8gQId3sd0uJ7PAxsDvyUW8JsPKKTz5JQ96wQNIgsXJGB/wLrtzXoxtXKfrgWJlDsbu7R7YgA&lt;span&gt;=&lt;/span&gt;
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;pre&gt;1
2
3
4
5
6
7
8
9
10
&lt;/pre&gt;&lt;/td&gt;&lt;td&gt;&lt;pre&gt;% curl &lt;span&gt;'https://sum.golang.org/lookup/github.com/!google/uuid@v1.6.0'&lt;/span&gt;
22198707
github.com/Google/uuid v1.6.0 h1:2avh7oGmXo3QQBdhUzCNHa3t06F22DZJzaton5Cp5pc&lt;span&gt;=&lt;/span&gt;
github.com/Google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo&lt;span&gt;=&lt;/span&gt;

go.sum database tree
48547430
&lt;span&gt;nz45epGyfKdGJiU29VkyCZWYt8AvqzpTogcry0Ck9m0&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;

— sum.golang.org Az3grr5LY/709J21wXS58h4kPuL0EUaVLrFdpHpRQiwfY9T7lh3gg71GDs3J40Kg6GDhj3XFEPt4u+6n4npgGNTemAg&lt;span&gt;=&lt;/span&gt;
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Observe that the two have different log entry numbers (&lt;code&gt;22152757&lt;/code&gt; vs &lt;code&gt;22198707&lt;/code&gt;),
despite pointing to the same logical module contents. However, they &lt;em&gt;also&lt;/em&gt; have
different content hashes (the first &lt;code&gt;h1:...&lt;/code&gt;), but &lt;strong&gt;not&lt;/strong&gt; different &lt;code&gt;go.mod&lt;/code&gt; hashes.
This is because the content hash includes the module path as part of its input,
while the &lt;code&gt;go.mod&lt;/code&gt; hash is the hash of the &lt;code&gt;go.mod&lt;/code&gt; file itself, as
it appears in the logical module contents.&lt;/p&gt;
&lt;p&gt;Moreover, we can induce a &lt;em&gt;third&lt;/em&gt; log entry by requesting the module
version from the proxy using yet another URL form, such as
&lt;code&gt;https://proxy.golang.org/github.com/GOOgle/uuid/@v1.&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;pre&gt;1
2
3
4
5
6
7
8
9
10
&lt;/pre&gt;&lt;/td&gt;&lt;td&gt;&lt;pre&gt;% curl &lt;span&gt;'https://sum.golang.org/lookup/github.com/!g!o!ogle/uuid@v1.6.0'&lt;/span&gt;
48547565
github.com/GOOgle/uuid v1.6.0 h1:pwtfSDjGACNyzYVFI0EQMg8KweQ5T+2NrQKJVXoKyj0&lt;span&gt;=&lt;/span&gt;
github.com/GOOgle/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo&lt;span&gt;=&lt;/span&gt;

go.sum database tree
48547566
4x18IY7uNfTK8JVpFSg93+aVFGUoS9slyyVfEn5PRWY&lt;span&gt;=&lt;/span&gt;

— sum.golang.org Az3grgmqwzBbDjb7ojZhmPLDCyCCAreUMfcQ1b7slVrxhFAS8JpZ8wJvGqZ1FW5a8t/FxUXbnQYD2s/+V7RQICZHHgU&lt;span&gt;=&lt;/span&gt;
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This last one took a few seconds to respond&lt;sup&gt;&lt;a href="https://blog.yossarian.net/2025/12/29/Some-flexibility-with-Go-s-sumdb#fn:sync"&gt;7&lt;/a&gt;&lt;/sup&gt;, unlike the other two. As the new log entry
number close to the signed tree head indicates, it was created on demand (since I was the first to
request it).&lt;/p&gt;
&lt;h2&gt;Implications&lt;a href="https://blog.yossarian.net/2025/12/29/Some-flexibility-with-Go-s-sumdb#implications"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;To tie things together:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Go sumdb log entries are unique per module version;&lt;/li&gt;
&lt;li&gt;Go module paths (within the module version) are case-sensitive;&lt;/li&gt;
&lt;li&gt;Module paths are URLs and the path component of URLs is
&lt;em&gt;case-indeterminate&lt;/em&gt;&lt;sup&gt;&lt;a href="https://blog.yossarian.net/2025/12/29/Some-flexibility-with-Go-s-sumdb#fn:host"&gt;8&lt;/a&gt;&lt;/sup&gt;, meaning that multiple case forms
&lt;em&gt;may&lt;/em&gt; point to the same module version contents;&lt;/li&gt;
&lt;li&gt;(1)-(3) mean that the Go proxy can be induced into creating
multiple distinct log entries that point to the same module version contents,
albeit with different content hashes.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This has a few interesting consequences:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;In practice, monitoring a module via the sumdb is not as trivial as
monitoring a single representation: the monitor needs to be aware of all
valid case forms, which in turn depends on the host. GitHub for example
has case-insensitive paths, but other hosts may not. The simplest thing
to do is probably to match on any case-folded variant of the module path
being monitored, but that may yield false positives if the host is
case-sensitive.&lt;/p&gt;
&lt;p&gt;The main (public) Go module monitor that I’m aware of is &lt;a href="https://www.gopherwatch.org"&gt;GopherWatch&lt;/a&gt;,
 which &lt;a href="https://github.com/mjl-/gopherwatch/blob/26fe1de964d2b0ce14cbf0c507ca6757b38f8da6/data.go#L126-L129"&gt;intentionally only does case-sensitive comparisons&lt;/a&gt;. This is
 probably a reasonable default for the average user (who is likely monitoring
 modules on GitHub or GitLab), but it does mean that a malicious
 case-sensitive host can evade detection.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;src.example.com/Foo/bar&lt;/code&gt; and &lt;code&gt;src.example.com/foo/bar&lt;/code&gt; &lt;em&gt;really are&lt;/em&gt; distinct
modules from Go’s perspective, even though humans have been trained to treat
them as the same (thanks to common web URL practices).
An attacker could in principle take advantage of
this to do something similar to &lt;a href="https://en.wikipedia.org/wiki/Typosquatting"&gt;typosquatting&lt;/a&gt;, where an innocent-looking
rename of &lt;code&gt;src.example.com/acmecorp/widget&lt;/code&gt; to &lt;code&gt;src.example.com/AcmeCorp/widget&lt;/code&gt;
results in completely different module contents.&lt;/p&gt;
&lt;p&gt;Or in other words: ask yourself if this diff would attract your scrutiny
 in an otherwise unobjectionable change:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;pre&gt;1
2
&lt;/pre&gt;&lt;/td&gt;&lt;td&gt;&lt;pre&gt; - import "src.example.com/acmecorp/widget"
 + import "src.example.com/AcmeCorp/widget"
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt; 
&lt;p&gt;What makes this (potentially) more pernicious than “normal” typosquatting
 is that it looks unobjectionable in a way that normal typosquatting
 (like &lt;code&gt;src.example.com/acmec0rp/widget&lt;/code&gt;) wouldn’t. As evidenced by the log lookups
 earlier, it’s &lt;em&gt;already&lt;/em&gt; common enough for people to use different case
 forms (e.g. &lt;code&gt;google/uuid&lt;/code&gt; and &lt;code&gt;Google/uuid&lt;/code&gt; both already having lower entries).&lt;/p&gt;
&lt;p&gt;In practice, what holds this back is that the attacker would either need
 to control the host (to ensure that different cases route differently)
 &lt;em&gt;or&lt;/em&gt; find an existing host that does that. GitHub and GitLab both appear
 to guarantee case-insensitivity, but there may be others that don’t.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;For a module path containing &lt;code&gt;N&lt;/code&gt; non-domain characters in &lt;code&gt;[a-zA-Z]&lt;/code&gt;, there are
2&lt;sup&gt;N&lt;/sup&gt; possible case variants. This doesn’t pose a problem for matching
within the monitor (we can just compare case-insensitively), but it &lt;em&gt;does&lt;/em&gt;
pose fatigue and resource risks:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Using &lt;code&gt;src.example.com/acmecorp/widget&lt;/code&gt; as an example, there are 2&lt;sup&gt;14&lt;/sup&gt; = 16,384
possible case variants. An attacker who wants to obscure their activity against
that module could spam the proxy with requests for those variants, causing
alert fatigue (or dropped alerts due to volume) for the monitor.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Similarly, the attacker can force the creation of 2&lt;sup&gt;14&lt;/sup&gt; distinct log entries
for the same module version, bloating the log and wasting the proxy and log’s
compute and network resources. Anybody can already do this by submitting
junk modules to the proxy, but this is partcularly bad because it makes
amplification relatively easy: the attacker doesn’t have to create any additional
modules, and can even use pre-existing innocent module versions.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Of these, I think (2) is probably the most interesting:
(1) is something that monitors can fix by comparing module paths case-insensitively,
and (3) is just a griefing vector. The first half of (3) is also probably mitigable
by deduplicating on the &lt;code&gt;go.sum&lt;/code&gt;’s content hash, since the attacker can’t vary
that just by varying the module path’s case.&lt;/p&gt;
&lt;h2&gt;Concluding thoughts&lt;a href="https://blog.yossarian.net/2025/12/29/Some-flexibility-with-Go-s-sumdb#concluding-thoughts"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Does this matter from a security perspective? I think no, at least not much.
The typosquatting-style attack is interesting, but assumes things about the
module host that aren’t typically true. Specifically, it assumes a
case-sensitive module host where the attacker can &lt;em&gt;in fact&lt;/em&gt; register/emplace
modules with different case forms. That’s probably uncommon in practice,
and any host that’s observed to do this intentionally would probably be
worth excluding from the Go module proxy altogether.&lt;/p&gt;
&lt;p&gt;At the most, it’s a quirk and a reminder of how subtle “identity” can be
in a transparency scheme with additional uniqueness properties. Packaging
ecosystems that aim to adopt transparency logs in a manner similar to Go’s
sumdb will almost certainly run into similar issues.&lt;/p&gt;
&lt;p&gt;It’s also a great demonstration of how &lt;em&gt;good and thoughtful&lt;/em&gt; Go’s packaging
design is: the system as a whole lacks ambiguity except where introduced by
external interactions (host filesystems, URL paths), and those ambiguities are
carefully constrained down to managable levels.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;p.s. Thanks to &lt;a href="https://filippo.io"&gt;Filippo Valsorda&lt;/a&gt; for sanity-checking some of my conclusions here,
since I am by no means a Go expert.&lt;/p&gt;
&lt;hr&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;This happens for both innocent and malicious reasons, usually because Git tags
    are mutable and can be overwritten with a force-push. &lt;a href="https://blog.yossarian.net/2025/12/29/Some-flexibility-with-Go-s-sumdb#fnref:tag"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The semantics of including some data in a transparency log are
              governed by a &lt;a href="https://transparency.dev/pdfs/claimant-model-tutorial.pdf"&gt;claimant model&lt;/a&gt;, as well as how clients are
              expected to interact with that log. For example, interactions
              with CT logs are generally expected to be “total” in the sense
              that a leaf certificate that chains up to a public CA should
              always be logged, and the absence of an entry is a notable
              signal. In contrast, a packaging ecosystem that adopts Sigstore
              generally can’t (and shouldn’t) compel every single package
              to be signed and logged, so the absence of an entry is not
              necessarily notable absent other context. &lt;a href="https://blog.yossarian.net/2025/12/29/Some-flexibility-with-Go-s-sumdb#fnref:claimantmodel"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;This being how open source packaging works by design. &lt;a href="https://blog.yossarian.net/2025/12/29/Some-flexibility-with-Go-s-sumdb#fnref:design"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The difference between a &lt;em&gt;monitor&lt;/em&gt; and an &lt;em&gt;auditor&lt;/em&gt; is subtle:
         a &lt;em&gt;monitor&lt;/em&gt; observes the log for specific behaviors (e.g.
         new entries matching some set of claims), while an &lt;em&gt;auditor&lt;/em&gt;
         verifies the integrity/correctness of the log itself
         (e.g. ensuring the log is actually consistent and append-only). &lt;a href="https://blog.yossarian.net/2025/12/29/Some-flexibility-with-Go-s-sumdb#fnref:auditors"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;While writing this, I tried to find an resource that actually
           documents this property as guaranteed, but I couldn’t! However,
           it’s implied by the sumdb’s lookup API, which can only return
           a single entry per module version. It’s also mentioned indirectly
           in &lt;a href="https://raphting.dev/posts/gosumdb-live-again/"&gt;this blog post on raphting.dev&lt;/a&gt; and in Martin Hutchinson’s &lt;a href="https://github.com/mhutchinson/sumdb-audit"&gt;&lt;code&gt;sumdb-audit&lt;/code&gt;&lt;/a&gt;
           tool. The uniqueness check is also visible in the &lt;a href="https://cs.opensource.google/go/x/mod/+/refs/tags/v0.31.0:sumdb/test.go;l=96-101"&gt;TestServer in x/mod/sumdb&lt;/a&gt;.
           After discussion with Filippo, I understand why this isn’t ever described as
           “guaranteed”: uniqueness is a property that’s enforced by auditors, the
           log &lt;strong&gt;cannot&lt;/strong&gt; itself guarantee it. &lt;a href="https://blog.yossarian.net/2025/12/29/Some-flexibility-with-Go-s-sumdb#fnref:uniqueness"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The &lt;code&gt;https://&lt;/code&gt; is implied. &lt;a href="https://blog.yossarian.net/2025/12/29/Some-flexibility-with-Go-s-sumdb#fnref:https"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Unlike Certificate Transpareny, Go’s sumdb inclusion is synchronous: the proxy
     blocks until the log entry is actually fully included, instead of yielding
     a promise to include it later. I suspect the proxy is additionally fully fetching
     the module itself for hashing, which adds more synchronous delay. &lt;a href="https://blog.yossarian.net/2025/12/29/Some-flexibility-with-Go-s-sumdb#fnref:sync"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;In the sense that the path component of a URL is case sensitive, but that
     it’s up to the handling HTTP server/host to determine how it handles
     case sensitivity. &lt;a href="https://blog.yossarian.net/2025/12/29/Some-flexibility-with-Go-s-sumdb#fnref:host"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;hr&gt;
&lt;span&gt;
  Discussions:
  
  &lt;a href="https://www.reddit.com/r/enosuchblog/comments/1pypbhh/some_flexibility_with_gos_sumdb/"&gt;Reddit&lt;/a&gt;
&lt;a href="https://infosec.exchange/@yossarian/115803589597737254"&gt;Mastodon&lt;/a&gt;
&lt;a href="https://bsky.app/profile/yossarian.net/post/3mb55zn6bqj2u"&gt;Bluesky&lt;/a&gt;
&lt;/span&gt;
&lt;hr&gt;

&lt;a href="https://blog.yossarian.net/2025/12/13/cooldowns-redux"&gt;Previously&lt;/a&gt;


&lt;a href="https://blog.yossarian.net/2026/04/11/Brocards-for-vulnerability-triage"&gt;Newer&lt;/a&gt;

&lt;/body&gt;
</ns0:encoded><pubDate>Mon, 29 Dec 2025 00:00:00 UTC</pubDate></item><item><title>Dependency cooldowns, redux</title><link>https://blog.yossarian.net/2025/12/13/cooldowns-redux</link><description>Three weeks ago I wrote about how we should all be using dependency cooldowns.</description><ns0:encoded xmlns:ns0="http://purl.org/rss/1.0/modules/content/">&lt;body morss_own_score="2.592233009708738" morss_score="76.07895404228734"&gt;
&lt;h1&gt;ENOSUCHBLOG&lt;/h1&gt;
&lt;h2&gt;&lt;em&gt;Programming, philosophy, pedaling.&lt;/em&gt;&lt;/h2&gt;
&lt;hr&gt;
&lt;h1&gt;
&lt;a href="https://blog.yossarian.net/2025/12/13/cooldowns-redux"&gt;Dependency cooldowns, redux&lt;/a&gt;
&lt;/h1&gt;
&lt;p&gt;
&lt;em&gt;Dec 13, 2025&lt;/em&gt;

       

    
      &lt;span&gt;
        Tags:
        
        
          &lt;a href="https://blog.yossarian.net/tags#oss"&gt;oss&lt;/a&gt;,
        
          &lt;a href="https://blog.yossarian.net/tags#security"&gt;security&lt;/a&gt;
&lt;/span&gt;
    

       

    
  &lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Three weeks ago I wrote about how &lt;a href="https://blog.yossarian.net/2025/11/21/We-should-all-be-using-dependency-cooldowns"&gt;we should all be using dependency cooldowns&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This got a lot of attention, which is great! So I figured I owe you, dear reader,
an update with (1) some answers to common questions I’ve received, and (2) some
updated on movements in large open source ecosystems since the post’s publication.&lt;/p&gt;
&lt;h2&gt;Common questions and answers&lt;a href="https://blog.yossarian.net/2025/12/13/cooldowns-redux#common-questions-and-answers"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Question&lt;/strong&gt;: Aren’t cooldowns a self-defeating policy? In other words:
if everyone uses cooldowns wouldn’t we be in the exact same situation, just shifted
back by the cooldown period?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Answer&lt;/strong&gt;: I think there are two parts to this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;The observation in the original post is that there are parties other than
downstreams in the open source ecosystem, namely secuity partners (vendors) and index
maintainers themselves. These parties have &lt;em&gt;strong incentives&lt;/em&gt; to proactively
monitor, report, and remove malicious packages from the ecosystem. Most importantly,
these incentives are timely, even when user installations are not.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Even with a universal exhortation to use cooldowns, I think universal adoption
is clearly not realistic: there are &lt;em&gt;always&lt;/em&gt; going to be people who live at
the edge. If those people want to be the proverbial canaries in the coal mine, that’s
their prerogative!&lt;/p&gt;
&lt;p&gt;Or in other words, we certainly all &lt;em&gt;should&lt;/em&gt; be using cooldowns, but clearly
that’s never going to happen.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Question&lt;/strong&gt;: What about security updates? Wouldn’t cooldowns delay important security patches?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Answer&lt;/strong&gt;: I guess so, but you shouldn’t do that! Cooldowns are a &lt;em&gt;policy&lt;/em&gt;, and all
policies have escape hatches. The original post itself notes an important escape hatch
that already exists in how cooldowns are implemented in tools like Dependabot:
cooldowns &lt;strong&gt;don’t apply&lt;/strong&gt; to security updates.&lt;/p&gt;
&lt;p&gt;In other words: ecosystems that are considering implementing cooldowns directly in their
packaging tools should make sure that users can encode exceptions&lt;sup&gt;&lt;a href="https://blog.yossarian.net/2025/12/13/cooldowns-redux#fn:exceptions"&gt;1&lt;/a&gt;&lt;/sup&gt; as necessary.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Question&lt;/strong&gt;: Doesn’t this incentivize attackers to abuse the vulnerability disclosure process?
In other words, what stops an attacker from reporting their own malicious release as a
vulnerability fix in order to bypass cooldowns?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Answer&lt;/strong&gt;: Nothing, in principle: this certainly does make vulnerability disclosures themselves
an attractive mechanism for bypassing cooldowns. However, in practice, I think an attacker’s
ability to do this is limited by (at least) three factors:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Creating a public vulnerability disclosure on a project (e.g. via &lt;a href="https://docs.github.com/en/code-security/security-advisories/about-github-security-advisories"&gt;GitHub Security Advisories&lt;/a&gt;)
&lt;em&gt;generally&lt;/em&gt; requires a higher degree of privilege/comprehensive takeover than simply
publishing a malicious version. Specifically: most of the malicious publishing activity
we’ve seen thus far involves a compromised long-lived publishing credential (e.g. an npm
or PyPI API token), rather than full account takeover. We might seek that kind of full
ATO in the future, but it’s a significantly higher bar (particularly in the presence
of in-session MFA challenges on GitHub).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The name of the game continues to be &lt;em&gt;maximizing&lt;/em&gt; the window of opportunity, which is often
shorter than a single day. At this timescale, hours are significant. But fortunately&lt;sup&gt;&lt;a href="https://blog.yossarian.net/2025/12/13/cooldowns-redux#fn:fortunate"&gt;2&lt;/a&gt;&lt;/sup&gt; for us,
&lt;em&gt;propagating&lt;/em&gt; a public vulnerability disclosure takes a nontrivial amount of time: CVEs take a
nontrivial amount of time to assign&lt;sup&gt;&lt;a href="https://blog.yossarian.net/2025/12/13/cooldowns-redux#fn:ghsa"&gt;3&lt;/a&gt;&lt;/sup&gt;, and ecosystem-level propagation of vulnerability information
(e.g. into RUSTSEC or PYSEC) typically happens on the timeframe of hours (via scheduled batch jobs).&lt;/p&gt;
&lt;p&gt;Consequently, abusing the vulnerability disclosure process to bypass cooldowns requires the
attacker to shorten their window of opportunity, which isn’t in their interest. That doesn’t
mean they &lt;em&gt;won’t&lt;/em&gt; do it (especially as the update loop between advisories and ecosystem
vulnerability databases gets shorter), but it &lt;em&gt;does&lt;/em&gt; stand to reason that it disincentivizes
this kind of abuse to some degree.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Stealth. Creating a public vulnerability disclosure is essentially a giant flashing neon sign
to security-interested parties to look &lt;em&gt;extremely closely&lt;/em&gt; at a given release.
More specifically, it’s a signal to those parties that they should &lt;em&gt;diff&lt;/em&gt; the new release
against the old (putatively vulnerable) one, to look for the vulnerable code. This is the exact
opposite of what the attacker wants: they’re trying to sneak malicious code &lt;em&gt;into&lt;/em&gt; the new
release, and are trying to avoid drawing attention to it.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Question&lt;/strong&gt;: What about &lt;em&gt;opportunistic&lt;/em&gt; abuse of the vulnerability disclosure process?
For example, if &lt;code&gt;1.2.3&lt;/code&gt; is vulnerable and &lt;code&gt;1.2.4&lt;/code&gt; is a &lt;em&gt;legitimate&lt;/em&gt; security update, what stops
the attacker from publishing &lt;code&gt;1.2.5&lt;/code&gt; with malicious code immediately after &lt;code&gt;1.2.4&lt;/code&gt;?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Answer&lt;/strong&gt;: This is a great example of why cooldown policies (and their bypasses)
struggle to be universal without human oversight. Specifically, it demonstrates why bypasses should
probably be &lt;em&gt;minimal by default&lt;/em&gt;: if both &lt;code&gt;1.2.4&lt;/code&gt; and &lt;code&gt;1.2.5&lt;/code&gt; claim to address a vulnerability and
both require bypassing the cooldown, then selecting the lower version is &lt;em&gt;probably&lt;/em&gt; the more
correct choice in an automatic dependency updating context.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Ecosystem updates&lt;a href="https://blog.yossarian.net/2025/12/13/cooldowns-redux#ecosystem-updates"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;This is a somewhat free-form section for ecosystem changes I’ve noticed since the original post.&lt;/p&gt;
&lt;p&gt;If there are others I’ve missed, please let me know!&lt;/p&gt;
&lt;h3&gt;Python&lt;a href="https://blog.yossarian.net/2025/12/13/cooldowns-redux#python"&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://docs.astral.sh/uv/"&gt;uv&lt;/a&gt; has added support for dependency cooldowns via relative &lt;code&gt;--exclude-newer&lt;/code&gt; values.
Recently released versions of uv already include this feature.&lt;/p&gt;
&lt;p&gt;For example, a user can do &lt;code&gt;--exclude-newer=P7D&lt;/code&gt; to exclude any dependency updates
published within the last seven days.&lt;/p&gt;
&lt;p&gt;Documentation: &lt;a href="https://docs.astral.sh/uv/concepts/resolution/#dependency-cooldowns"&gt;uv - dependency cooldowns&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;References: &lt;a href="https://github.com/astral-sh/uv/pull/16814"&gt;astral-sh/uv#16814&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://pip.pypa.io/en/stable/"&gt;pip&lt;/a&gt; is adding an absolute point-in-time feature via &lt;code&gt;--uploaded-prior-to&lt;/code&gt;.
This is currently slated to be released with pip 26, i.e. early next year.&lt;/p&gt;
&lt;p&gt;In addition to the “absolute” cooldown feature above, pip is also considering
adding a relative cooldown feature similar to uv’s. This is being tracked in
&lt;a href="https://github.com/pypa/pip/issues/13674"&gt;pypa/pip#13674&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;References: &lt;a href="https://github.com/pypa/pip/pull/13625"&gt;pypa/pip#13625&lt;/a&gt;, &lt;a href="https://github.com/pypa/pip/issues/13674"&gt;pypa/pip#13674&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Rust&lt;a href="https://blog.yossarian.net/2025/12/13/cooldowns-redux#rust"&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;cargo is discussing a design for cooldowns in
&lt;a href="https://github.com/rust-lang/cargo/issues/15973"&gt;rust-lang/cargo#15973&lt;/a&gt;. This discussion predates
my blog post, but appears to have been reinvigorated by it.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Ruby&lt;a href="https://blog.yossarian.net/2025/12/13/cooldowns-redux#ruby"&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;There’s been some discussion in the Bundler community Slack about adding
cooldowns directly to the &lt;a href="https://gem.coop"&gt;gem.coop&lt;/a&gt; index, e.g. providing index
“views” like &lt;code&gt;/view/cooldown/7d/&lt;/code&gt; for index-level cooldowns. I think this is a very
cool approach!&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;.NET&lt;a href="https://blog.yossarian.net/2025/12/13/cooldowns-redux#net"&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;The NuGet community is discussing a cooldown design in
&lt;a href="https://github.com/NuGet/Home/issues/14657"&gt;NuGet/Home#14657&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;JavaScript&lt;a href="https://blog.yossarian.net/2025/12/13/cooldowns-redux#javascript"&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;pnpm has had cooldowns since &lt;a href="https://pnpm.io/blog/releases/10.16"&gt;September with v10.16&lt;/a&gt;, with
&lt;a href="https://pnpm.io/settings#minimumreleaseage"&gt;&lt;code&gt;minimumReleaseAge&lt;/code&gt;&lt;/a&gt;! They even have a cooldown
exclusion feature.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;yarn added cooldown support one month after pnpm, via
&lt;a href="https://yarnpkg.com/configuration/yarnrc#npmMinimalAgeGate"&gt;&lt;code&gt;npmMinimalAgeGate&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;npm does not have cooldown support yet. There appear to be several discussions about it,
some of which date back years. &lt;a href="https://github.com/npm/rfcs/issues/646"&gt;npm/rfcs#646&lt;/a&gt; and
&lt;a href="https://github.com/npm/cli/issues/8570"&gt;npm/cli#8570&lt;/a&gt; appear to have most of the context.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/npm/cli/pull/8802"&gt;npm/cli#8802&lt;/a&gt; is also open adding an implementation
of the above RFC.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Go&lt;a href="https://blog.yossarian.net/2025/12/13/cooldowns-redux#go"&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Go is discussing the feasibility/applicability of cooldowns in 
&lt;a href="https://github.com/golang/go/issues/76485"&gt;golang/go#76485&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;GitHub Actions&lt;a href="https://blog.yossarian.net/2025/12/13/cooldowns-redux#github-actions"&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://github.com/suzuki-shunsuke/pinact"&gt;pinact&lt;/a&gt; has added a &lt;a href="https://github.com/suzuki-shunsuke/pinact#skip-recently-released-versions"&gt;&lt;code&gt;--min-age&lt;/code&gt; flag&lt;/a&gt;
to support cooldowns for GitHub Actions dependencies.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Renovate and Dependabot already do a decent job of providing cooldowns for GitHub Actions updates,
including support for hash-pinning &lt;em&gt;and&lt;/em&gt; updating the associated version comment.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;pre-commit&lt;a href="https://blog.yossarian.net/2025/12/13/cooldowns-redux#pre-commit"&gt;#&lt;/a&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://github.com/j178/prek"&gt;prek&lt;/a&gt; (a pre-commit implementation in Rust) has added a &lt;code&gt;--cooldown-days&lt;/code&gt; option in
&lt;a href="https://github.com/j178/prek/releases/tag/v0.2.22"&gt;release 0.2.22&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Thanks to &lt;a href="https://github.com/hugovk"&gt;Hugo van Kemenade&lt;/a&gt; for pointing this out to me!&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Security updates are the most obvious exception, but it also seems reasonable to me to
           allow people to encode exceptions for data-only dependencies, first-party dependencies,
           trusted dependencies, and so forth. It’s clearly a non-trivial and non-generalizable
           problem! &lt;a href="https://blog.yossarian.net/2025/12/13/cooldowns-redux#fnref:exceptions"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;For some definition of “fortunate”: clearly we want TTLs for vulnerability disclosures
          to be as short as possible in normal, non-malicious circumstances! &lt;a href="https://blog.yossarian.net/2025/12/13/cooldowns-redux#fnref:fortunate"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Unlike GHSAs, which are typically assigned instantly. For better or worse however, CVE IDs
     continue to be the “reference” identifier for ecosystem-level vulnerability information
     propagation. &lt;a href="https://blog.yossarian.net/2025/12/13/cooldowns-redux#fnref:ghsa"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;hr&gt;
&lt;span&gt;
  Discussions:
  
  &lt;a href="https://www.reddit.com/r/enosuchblog/comments/1plq3rm/dependency_cooldowns_redux/"&gt;Reddit&lt;/a&gt;
&lt;a href="https://infosec.exchange/@yossarian/115713309903454408"&gt;Mastodon&lt;/a&gt;
&lt;a href="https://bsky.app/profile/yossarian.net/post/3m7v336n7on2q"&gt;Bluesky&lt;/a&gt;
&lt;/span&gt;
&lt;hr&gt;

&lt;a href="https://blog.yossarian.net/2025/11/21/We-should-all-be-using-dependency-cooldowns"&gt;Previously&lt;/a&gt;


&lt;a href="https://blog.yossarian.net/2025/12/29/Some-flexibility-with-Go-s-sumdb"&gt;Newer&lt;/a&gt;

&lt;/body&gt;
</ns0:encoded><pubDate>Sat, 13 Dec 2025 00:00:00 UTC</pubDate></item><item><title>We should all be using dependency cooldowns</title><link>https://blog.yossarian.net/2025/11/21/We-should-all-be-using-dependency-cooldowns</link><description>TL;DR: Dependency cooldowns are a free, easy, and incredibly effective way to mitigate the large majority of open source supply chain attacks. More individual projects should apply cooldowns (via tools like Dependabot and Renovate) to their dependencies, and packaging ecosystems should invest in first-class support for cooldowns directly in their package managers.</description><ns0:encoded xmlns:ns0="http://purl.org/rss/1.0/modules/content/">&lt;body morss_own_score="2.632867132867133" morss_score="70.54671165902025"&gt;
&lt;h1&gt;ENOSUCHBLOG&lt;/h1&gt;
&lt;h2&gt;&lt;em&gt;Programming, philosophy, pedaling.&lt;/em&gt;&lt;/h2&gt;
&lt;hr&gt;
&lt;h1&gt;
&lt;a href="https://blog.yossarian.net/2025/11/21/We-should-all-be-using-dependency-cooldowns"&gt;We should all be using dependency cooldowns&lt;/a&gt;
&lt;/h1&gt;
&lt;p&gt;
&lt;em&gt;Nov 21, 2025&lt;/em&gt;

       

    
      &lt;span&gt;
        Tags:
        
        
          &lt;a href="https://blog.yossarian.net/tags#oss"&gt;oss&lt;/a&gt;,
        
          &lt;a href="https://blog.yossarian.net/tags#security"&gt;security&lt;/a&gt;
&lt;/span&gt;
    

       

    
  &lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: Dependency cooldowns are a free, easy, and &lt;strong&gt;incredibly effective&lt;/strong&gt;
way to mitigate the &lt;em&gt;large majority&lt;/em&gt; of open source supply chain attacks.
More individual projects should apply cooldowns (via tools like Dependabot
and Renovate) to their dependencies, and packaging ecosystems should invest
in first-class support for cooldowns directly in their package managers.&lt;/p&gt;
&lt;p&gt;Some resources for adding cooldowns:&lt;/p&gt;

&lt;p&gt;See &lt;a href="https://blog.yossarian.net/2025/12/13/cooldowns-redux"&gt;part two&lt;/a&gt; as well for some responses to common questions about this
post, as well as updates on how various ecosystems have adopted cooldowns.&lt;/p&gt;

&lt;hr&gt;
&lt;p&gt;“Supply chain security” is a serious problem. It’s also &lt;strong&gt;seriously overhyped&lt;/strong&gt;,
in part because dozens of vendors have a vested financial interest in
convincing your that their &lt;em&gt;framing&lt;/em&gt; of the underlying problem&lt;sup&gt;&lt;a href="https://blog.yossarian.net/2025/11/21/We-should-all-be-using-dependency-cooldowns#fn:problem"&gt;1&lt;/a&gt;&lt;/sup&gt; is (1)
correct, and (2) worth your money.&lt;/p&gt;
&lt;p&gt;What’s consternating about this is that most open source supply chain
attacks have the same basic structure:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;An attacker compromises a popular open source project, typically via
a stolen credential or CI/CD vulnerabilty (such as &lt;a href="https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/"&gt;“pwn requests”&lt;/a&gt; in
GitHub Actions).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The attacker introduces a malicious change to the project and uploads
it somewhere that will have &lt;strong&gt;maximum effect&lt;/strong&gt; (PyPI, npm, GitHub releases,
&amp;amp;c., depending on the target).&lt;/p&gt;
&lt;p&gt;At this point, the &lt;em&gt;clock has started&lt;/em&gt;, as the attacker has moved
 into the public.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Users pick up the compromised version of the project via automatic
dependency updates or a lack of dependency pinning.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Meanwhile, the aforementioned vendors are scanning public indices
as well as customer repositories for signs of compromise, and
provide alerts upstream (e.g. to PyPI).&lt;/p&gt;
&lt;p&gt;Notably, vendors are &lt;em&gt;incentivized&lt;/em&gt; to report quickly and loudly upstream,
 as this increases the perceived value of their services in a crowded
 field.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Upstreams (PyPI, npm, &amp;amp;c.) remove or disable the compromised package
version(s).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;End-user remediation begins.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The key thing to observe is that the gap between (1) and (2) can be very large&lt;sup&gt;&lt;a href="https://blog.yossarian.net/2025/11/21/We-should-all-be-using-dependency-cooldowns#fn:gap"&gt;2&lt;/a&gt;&lt;/sup&gt;
(weeks or months), while the gap between (2) and (5) is &lt;strong&gt;typically very small&lt;/strong&gt;:
hours or days. This means that, once the attacker has moved into the actual
exploitation phase, their &lt;em&gt;window of opportunity&lt;/em&gt; to cause damage is pretty limited.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://blog.yossarian.net/assets/supply-chain-attack-timeline.png"&gt;&lt;/p&gt;
&lt;p&gt;We can see this with numerous prominent supply chain attacks over the last 18 months&lt;sup&gt;&lt;a href="https://blog.yossarian.net/2025/11/21/We-should-all-be-using-dependency-cooldowns#fn:filippo"&gt;3&lt;/a&gt;&lt;/sup&gt;:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Attack&lt;/th&gt;
&lt;th&gt;Approx. Window of Opportunity&lt;/th&gt;
&lt;th&gt;References&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;xz-utils&lt;/td&gt;
&lt;td&gt;≈ 5 weeks&lt;sup&gt;&lt;a href="https://blog.yossarian.net/2025/11/21/We-should-all-be-using-dependency-cooldowns#fn:outlier"&gt;4&lt;/a&gt;&lt;/sup&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://research.swtch.com/xz-timeline"&gt;Source&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ultralytics (phase 1)&lt;/td&gt;
&lt;td&gt;12 hours&lt;/td&gt;
&lt;td&gt;&lt;a href="https://blog.yossarian.net/2024/12/06/zizmor-ultralytics-injection#appendix-rough-timeline-of-events"&gt;Source&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ultralytics (phase 2)&lt;/td&gt;
&lt;td&gt;1 hour&lt;/td&gt;
&lt;td&gt;&lt;a href="https://blog.yossarian.net/2024/12/06/zizmor-ultralytics-injection#appendix-rough-timeline-of-events"&gt;Source&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tj-actions&lt;/td&gt;
&lt;td&gt;3 days&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.legitsecurity.com/blog/github-actions-tj-actions-changed-files-attack"&gt;Source&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;chalk&lt;/td&gt;
&lt;td&gt;&amp;lt; 12 hours&lt;/td&gt;
&lt;td&gt;&lt;a href="https://cycode.com/blog/npm-debug-chalk-supply-chain-attack-the-complete-guide/"&gt;Source&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nx&lt;/td&gt;
&lt;td&gt;4 hours&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.stepsecurity.io/blog/supply-chain-security-alert-popular-nx-build-system-package-compromised-with-data-stealing-malware"&gt;Source&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rspack&lt;/td&gt;
&lt;td&gt;1 hour&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/web-infra-dev/rspack/releases/tag/v1.1.8"&gt;Source&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;num2words&lt;/td&gt;
&lt;td&gt;&amp;lt; 12 hours&lt;/td&gt;
&lt;td&gt;&lt;a href="https://blog.pypi.org/posts/2025-07-31-incident-report-phishing-attack/#impact-analysis"&gt;Source&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kong Ingress Controller&lt;/td&gt;
&lt;td&gt;≈ 10 days&lt;/td&gt;
&lt;td&gt;&lt;a href="https://konghq.com/blog/product-releases/december-2024-unauthorized-kong-ingress-controller-3-4-0-build"&gt;Source&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;web3.js&lt;/td&gt;
&lt;td&gt;5 hours&lt;/td&gt;
&lt;td&gt;&lt;a href="https://threats.wiz.io/all-incidents/solana-web3js-supply-chain-attack"&gt;Source&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;(Each of these attacks has significant downstream effect, of course, but only
&lt;em&gt;within&lt;/em&gt; their window of opportunity. Subsequent compromises from each, like
&lt;a href="https://www.wiz.io/blog/shai-hulud-npm-supply-chain-attack"&gt;Shai-Hulud&lt;/a&gt;, represent &lt;em&gt;new&lt;/em&gt; windows of opportunity where the attackers regrouped
and pivoted onto the &lt;em&gt;next&lt;/em&gt; set of compromised credentials.)&lt;/p&gt;
&lt;p&gt;My takeaway from this: some windows of opportunity are bigger, but the &lt;em&gt;majority&lt;/em&gt;
of them are under a week long. Consequently, ordinary developers can &lt;em&gt;avoid
the bulk&lt;/em&gt; of these types of attacks by instituting &lt;strong&gt;cooldowns&lt;/strong&gt; on their dependencies.&lt;/p&gt;
&lt;h2&gt;Cooldowns&lt;a href="https://blog.yossarian.net/2025/11/21/We-should-all-be-using-dependency-cooldowns#cooldowns"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;A “cooldown” is exactly what it sounds like: a window of time between when a dependency
is published and when it’s considered suitable for use. The dependency is public during
this window, meaning that “supply chain security” vendors can work their magic
while the rest of us wait any problems out.&lt;/p&gt;
&lt;p&gt;I &lt;strong&gt;love&lt;/strong&gt; cooldowns for several reasons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;They’re empirically effective, per above. They won’t stop &lt;em&gt;all&lt;/em&gt; attackers,
but they &lt;em&gt;do&lt;/em&gt; stymie the majority of high-visibiity, mass-impact supply chain
attacks that have become more common.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;They’re &lt;em&gt;incredibly&lt;/em&gt; easy to implement. Moreover, they’re &lt;strong&gt;literally free&lt;/strong&gt;
to implement in most cases: most people can use &lt;a href="https://docs.github.com/en/code-security/dependabot/working-with-dependabot/dependabot-options-reference#cooldown-"&gt;Dependabot’s functionality&lt;/a&gt;,
&lt;a href="https://docs.renovatebot.com/key-concepts/minimum-release-age/"&gt;Renovate’s functionality&lt;/a&gt;, or the functionality build directly into their
package manager&lt;sup&gt;&lt;a href="https://blog.yossarian.net/2025/11/21/We-should-all-be-using-dependency-cooldowns#fn:pkgmanager"&gt;5&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;This is how simple it is in Dependabot:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;pre&gt;1
2
3
4
5
6
7
8
9
&lt;/pre&gt;&lt;/td&gt;&lt;td&gt;&lt;pre&gt;  &lt;span&gt;version&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;2&lt;/span&gt;

  &lt;span&gt;# update once a week, with a 7-day cooldown&lt;/span&gt;
  &lt;span&gt;-&lt;/span&gt; &lt;span&gt;package-ecosystem&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;github-actions&lt;/span&gt;
    &lt;span&gt;directory&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;/&lt;/span&gt;
    &lt;span&gt;schedule&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;
      &lt;span&gt;interval&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;weekly&lt;/span&gt;
    &lt;span&gt;cooldown&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;
      &lt;span&gt;default-days&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;7&lt;/span&gt;
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt; 
&lt;p&gt;(Rinse and repeat for other ecosystems as needed.)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Cooldowns &lt;strong&gt;enforce positive behavior&lt;/strong&gt; from supply chain security vendors:
vendors are still incentivized to discover and report attacks quickly,
but are &lt;em&gt;not&lt;/em&gt; as incentivized to emit volumes of blogspam about “critical”
attacks on largely underfunded open source ecosystems.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Concluding / assorted thoughts&lt;a href="https://blog.yossarian.net/2025/11/21/We-should-all-be-using-dependency-cooldowns#concluding--assorted-thoughts"&gt;#&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;In the very small sample set above, 8/10 attacks had windows of opportunity
of less than a week. Setting a cooldown of 7 days would have prevented
the vast majority of these attacks from reaching end users (and causing
knock-on attacks, which several of these were). Increasing the cooldown to 14
days would have prevented all but 1 of these attacks&lt;sup&gt;&lt;a href="https://blog.yossarian.net/2025/11/21/We-should-all-be-using-dependency-cooldowns#fn:apt"&gt;6&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;Cooldowns are, obviously, &lt;strong&gt;not a panacea&lt;/strong&gt;: some attackers &lt;em&gt;will&lt;/em&gt; evade detection,
and delaying the inclusion of potentially malicious dependencies by a week
(or two) does not fundamentally alter the fact that supply chain security is a
&lt;em&gt;social trust&lt;/em&gt; problem, not a purely technical one. Still, an 80-90% reduction
in exposure through a technique that is free and easy seems hard to beat.&lt;/p&gt;
&lt;p&gt;Related to the above, it’s unfortunate that cooldowns aren’t baked &lt;em&gt;directly&lt;/em&gt;
into more packaging ecosystems: Dependabot and Renovate are great, but
&lt;em&gt;even better&lt;/em&gt; would be if the package manager itself (as the source of ground
truth) could enforce cooldowns directly (including of dependencies not
introduced or bumped through automated flows).&lt;/p&gt;
&lt;hr&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;The problem being, succinctly: modern software stacks are complex and opaque,
        with little to no difference in privilege between first-party code and third-party
        dependencies. &lt;a href="https://blog.yossarian.net/2025/11/21/We-should-all-be-using-dependency-cooldowns#fnref:problem"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;In part because of the prevalence of long-lived, overscoped credentials. Long-lived
    credentials let attackers operate on their own (comfortable) timelines; this is why
    &lt;a href="https://docs.pypi.org/trusted-publishers/"&gt;Trusted Publishing&lt;/a&gt; is such a useful (but not wholly sufficient)
    technique for reducing the attacker’s &lt;em&gt;attack staging window&lt;/em&gt;. &lt;a href="https://blog.yossarian.net/2025/11/21/We-should-all-be-using-dependency-cooldowns#fnref:gap"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Filippo Valsorda has an excellent compilation of recent supply
        chain compromises &lt;a href="https://words.filippo.io/compromise-survey/"&gt;here&lt;/a&gt;. &lt;a href="https://blog.yossarian.net/2025/11/21/We-should-all-be-using-dependency-cooldowns#fnref:filippo"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The xz-utils attack is a significant outlier, both in its scope and the length
        of its window of opportunity. In this case, I’ve measured from the attacker’s 
        first backdoored release (v5.6.0, 2024-02-24) to the time of rollback within 
        Debian (2024-03-28). &lt;a href="https://blog.yossarian.net/2025/11/21/We-should-all-be-using-dependency-cooldowns#fnref:outlier"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;For example, pnpm’s &lt;a href="https://pnpm.io/settings#minimumreleaseage"&gt;&lt;code&gt;minimumReleaseAge&lt;/code&gt;&lt;/a&gt;.
           uv also has &lt;a href="https://docs.astral.sh/uv/guides/scripts/#improving-reproducibility"&gt;&lt;code&gt;exclude-newer&lt;/code&gt;&lt;/a&gt;, 
           although this specifies an absolute cutoff rather than a rolling cooldown. &lt;a href="https://blog.yossarian.net/2025/11/21/We-should-all-be-using-dependency-cooldowns#fnref:pkgmanager"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Notably, the only attack that would have stymied a 14-day cooldown is xz-utils,
    which is &lt;em&gt;also&lt;/em&gt; the most technically, logistically, and socially advanced of all of the
    attacks. &lt;a href="https://blog.yossarian.net/2025/11/21/We-should-all-be-using-dependency-cooldowns#fnref:apt"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;hr&gt;
&lt;span&gt;
  Discussions:
  
  &lt;a href="https://www.reddit.com/r/enosuchblog/comments/1p30gsd/we_should_all_be_using_dependency_cooldowns/"&gt;Reddit&lt;/a&gt;
&lt;a href="https://infosec.exchange/@yossarian/115588225552391270"&gt;Mastodon&lt;/a&gt;
&lt;a href="https://bsky.app/profile/yossarian.net/post/3m65jjftw6q2j"&gt;Bluesky&lt;/a&gt;
&lt;/span&gt;
&lt;hr&gt;

&lt;a href="https://blog.yossarian.net/2025/09/22/dear-github-no-yaml-anchors"&gt;Previously&lt;/a&gt;


&lt;a href="https://blog.yossarian.net/2025/12/13/cooldowns-redux"&gt;Newer&lt;/a&gt;

&lt;/body&gt;
</ns0:encoded><pubDate>Fri, 21 Nov 2025 00:00:00 UTC</pubDate></item></channel></rss>