PURL: un nome univoco per ogni pacchetto
Log4Shell ha mostrato che molti non sapevano cosa ci fosse dentro il proprio software. Il PURL è il modo standard per dare un nome univoco a ogni pacchetto: com'è fatto, dove si usa, come si descrivono i range di versioni con VERS e cosa c'entra CPAN.
Questa è la prima issue vera: parliamo di una vulnerabilità catastrofica, di standard internazionali, di buone pratiche… e di Package URL, o PURL.
Per parlare di PURL dobbiamo tornare indietro di qualche anno, per la precisione al 9 dicembre 2021. A pochi giorni da Natale, aziende informatiche, telco, organizzazioni di ogni tipo e anche semplici utenti si sono trovati alle prese con una vulnerabilità che ha fatto tremare mezza internet.
Stiamo parlando di Log4Shell (CVE-2021-44228).
Log4Shell: “dove usiamo Log4j?”
In quei giorni molte organizzazioni si sono trovate davanti alla stessa domanda:
Il mio software usa Log4j? In quale versione, e dove?
Una delle difficoltà più grandi di quelle ore non era correggere il software, applicare la patch o la mitigazione, ma capire dove si trovasse la libreria. Senza un inventario degli asset o un archivio di SBOM (Software Bill of Materials), per molti non era possibile sapere con certezza quali componenti di terze parti venissero usati all’interno dei propri software, applicazioni, appliance, ecc.
Nelle prime ore dopo la pubblicazione della CVE, in attesa che i principali scanner trovassero un modo di rilevarla, molti sysadmin hanno scritto al volo degli script da lanciare su decine o centinaia di server, e tanti altri ne sono comparsi in rete. Con find, unzip, awk e grep si cercava la libreria in tutto il filesystem e più o meno tutti gli script somigliavano a questo:
| |
Lo script cerca i jar di Log4j per nome con find e, se il nome del file non contiene la versione (ad esempio log4j.jar), la legge dal MANIFEST.MF dentro il jar.
Esistevano poi script più accurati: per essere sicuri che il jar fosse quello vulnerabile, ne calcolavano lo SHA-256 e lo confrontavano con gli elenchi di hash delle versioni colpite, che circolavano già dal 10 dicembre.
Anche questo aveva un limite. Un log4j-core-2.14.1.jar scaricato da Maven Central e quello contenuto nella distribuzione binaria di Apache hanno lo stesso MANIFEST.MF e gli stessi file, ma SHA-256 diversi:
ade7402a70667a727635d5c4c29495f4ff96f061f12539763f6f123973b465b0 log4j-core-2.14.1.jar (Maven Central)
9da0f5ca7c8eab693d090ae759275b9db4ca5acdbcfe4a63d3871e0b17367463 log4j-core-2.14.1.jar (apache-log4j-2.14.1-bin)
L’hash identifica un file, non un pacchetto: la stessa versione di Log4j, assemblata in due modi diversi, ha due hash diversi.
In generale questi script funzionavano solo se il jar era lì, con il suo nome. Non trovavano Log4j dentro un war o un fat jar, né le copie rinominate o incorporate in altre librerie.
Altri cercavano i servizi vulnerabili dall’esterno, in rete: inviavano la stringa JNDI e aspettavano la chiamata di ritorno. Preso anch’io dalla voglia di dare una mano, il 13 dicembre avevo pubblicato nmap-log4shell, uno script NSE per Nmap che fa proprio questo.
C’era poi il problema opposto. Nelle prime ore molti applicavano la mitigazione indicata da Apache, cioè togliere brutalmente dal jar la classe JndiLookup:
| |
Il jar però restava con lo stesso nome e lo stesso MANIFEST.MF, quindi gli script che guardavano il nome del file o la versione nel manifest continuavano a segnalarlo come vulnerabile. Lo stesso succedeva con gli scanner che nel frattempo avevano aggiunto questo controllo: bastava un jar con log4j nel nome per segnalarlo.
Il zip -d cambiava anche lo SHA-256, quindi per gli script che controllavano l’hash un jar mitigato risultava vulnerabile per versione ma con un hash sconosciuto, e capire quali server andavano ancora sistemati non era per niente banale.
Log4Shell ha reso evidente a tutti quello che negli Stati Uniti si stava già chiedendo ai fornitori, cioè che servivano due strumenti:
- un registro che elenca le dipendenze e le librerie di terze parti, lo SBOM;
- un modo univoco per identificare questi componenti, il PURL.
[ INFO ] SBOM
Pochi mesi prima, a maggio 2021, un Executive Order del governo USA (il 14028) aveva chiesto di definire gli elementi minimi di uno SBOM, pubblicati poi dall’NTIA a luglio (The Minimum Elements For a Software Bill of Materials): una richiesta rivolta ai fornitori di software delle agenzie federali, che dopo Log4Shell è diventata un tema per tutti.
A luglio 2022 il Cyber Safety Review Board, nel suo primo rapporto (Review of the December 2021 Log4j Event), ha rilevato che nemmeno le organizzazioni che già usavano gli SBOM li avevano sfruttati per trovare le installazioni vulnerabili di Log4j: gli SBOM di allora erano poco uniformi, con campi descritti in modi diversi e spesso senza la versione dei componenti. Tra le raccomandazioni ha chiesto di migliorarne gli strumenti e di diffonderne l’uso.
In Europa il Cyber Resilience Act (Regolamento UE 2024/2847, in vigore dal 10 dicembre 2024) va oltre: chi mette sul mercato prodotti con elementi digitali deve redigere uno SBOM in un formato leggibile dalle macchine, con almeno le dipendenze di primo livello. Gli obblighi di segnalazione delle vulnerabilità valgono già dall’11 settembre 2026, gli altri dall’11 dicembre 2027.
Cos’è il PURL
Il Package URL (PURL) è una stringa che identifica un pacchetto software, qualunque sia l’ecosistema da cui arriva. Ha la forma di un URL, e le informazioni per riconoscere il pacchetto stanno nella stringa stessa.
Un PURL è composto da sette componenti:
scheme:type/namespace/name@version?qualifiers#subpath
Ogni componente è separato da un carattere preciso, in modo che il parsing della stringa non sia mai ambiguo:
| Componente | Obbligatorio | Descrizione |
|---|---|---|
scheme | sì | Sempre pkg. È già registrato presso IANA in forma provvisoria. |
type | sì | Il tipo di pacchetto, cioè l’ecosistema: maven, npm, pypi, cpan… |
namespace | no | Dipende dal type. Un prefisso del nome: il groupId di Maven, il proprietario di un’immagine Docker, un utente o un’organizzazione GitHub. |
name | sì | Il nome del pacchetto. |
version | no | La versione del pacchetto. |
qualifiers | no | Dipendono dal type e contengono informazioni aggiuntive come l’URL dell’artefatto, l’architettura, la distribuzione, il checksum, ecc. |
subpath | no | Un percorso dentro il pacchetto, relativo alla sua radice. |
I componenti vanno dal più importante, a sinistra, al meno importante, a destra.
Qualche esempio da ecosistemi diversi:
pkg:npm/%40angular/animation@12.3.1pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1pkg:pypi/requests@2.31.0pkg:deb/debian/curl@7.50.3-1?arch=i386&distro=jessiepkg:golang/google.golang.org/genproto#googleapis/api/annotationspkg:github/package-url/purl-specpkg:generic/openssl@3.0.0?download_url=...
Da convenzione a standard
Il PURL è stato ideato da Philippe Ombredanne, di AboutCode (il progetto dietro ScanCode), che a novembre 2017 ha pubblicato la prima bozza della specifica su GitHub (package-url/purl-spec). Da lì è cresciuto con il contributo della community ed è diventato uno standard Ecma, ECMA-427, gestito dal comitato tecnico TC54, lo stesso di CycloneDX. La prima edizione, di dicembre 2025, è curata da Philippe insieme a John Horan, Michael Herzog e Steve Springett (l’autore di CycloneDX); la seconda è attesa per dicembre 2026, ed è in corso anche l’iter per diventare standard ISO.
Lo standard definisce la sintassi comune e le regole di parsing ed encoding di una stringa PURL. Le regole di ogni ecosistema (i tipi: npm, maven, cpan…), in continua evoluzione, stanno invece in specifiche separate, una per tipo, che la community mantiene su package-url/purl-spec.
Perché non bastavano CPE e SWID
Prima del PURL esistevano già standard per identificare il software.
CPE (Common Platform Enumeration) è uno standard del NIST, usato ancora oggi dall’NVD (National Vulnerability Database) e nei record CVE per collegare software e vulnerabilità. Ha però dei limiti: identifica solo sistemi operativi, hardware e applicazioni, le stringhe sono complesse e poco leggibili, e non riesce a rappresentare i pacchetti dei vari ecosistemi (CPAN, npm, Maven…).
SWID (Software Identification Tags) si usa soprattutto in ambito enterprise, per l’inventario degli asset e la gestione delle licenze. È poco diffuso nel mondo open source e più adatto agli inventari di sistema che alle dipendenze di un’applicazione.
Per superare questi limiti alcuni strumenti hanno inventato sintassi proprie, non standard e non interoperabili. Tenable Nessus, per esempio, usa p-cpe, una variante di CPE per i pacchetti: funziona dentro Nessus, ma nessun altro strumento la riconosce.
Il PURL è nato per questo. Ecco la stessa versione di Log4j nei due formati:
CPE cpe:2.3:a:apache:log4j:2.14.1:-:*:*:*:*:*:*
PURL pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1
Il CPE descrive un prodotto: Apache Software Foundation Log4j 2.14.1, in una struttura fissa di campi (part, vendor, product, version, update e altri, quasi sempre vuoti con *). I nomi li assegna un dizionario centrale, gestito da NIST/NVD, e finché un prodotto non è registrato non ha un CPE.
Il PURL descrive un pacchetto, così come lo distribuisce il suo ecosistema: il jar log4j-core del gruppo org.apache.logging.log4j su Maven, cioè quello che finisce nelle dipendenze di un progetto. Non serve che qualcuno lo assegni, perché chiunque può costruirlo partendo dal registro in cui il pacchetto è pubblicato, e ottiene sempre la stessa stringa.
Dove si usa
Con uno SBOM che contiene i PURL, la domanda di quel dicembre diventa una ricerca: si cerca pkg:maven/org.apache.logging.log4j/log4j-core negli SBOM dei propri progetti e si confronta la versione con quelle vulnerabili.
Oggi il PURL si trova in molti standard e strumenti:
- SBOM: SPDX e CycloneDX lo usano per identificare le dipendenze.
- CSAF (Common Security Advisory Framework): lo standard OASIS per gli advisory di sicurezza supporta il PURL per i pacchetti e VERS per i range di versioni.
- OSV (Open Source Vulnerabilities): nel database OSV ogni pacchetto colpito ha il suo PURL (per Log4Shell,
pkg:maven/org.apache.logging.log4j/log4j-core). - CVE: dal CVE Record Format v5.2.0 un record CVE può indicare il pacchetto coinvolto con il suo PURL.
- Database: Sonatype OSS Index (oggi parte di Sonatype Guide), VulnerableCode.
- Distribuzioni: NixOS ha il PURL nei metadati dei pacchetti di nixpkgs (
meta.identifiers.purl), e il supporto ai pacchetti Perl con il tipocpanè in arrivo. - Strumenti: Syft, Grype, OWASP Dependency-Track, osv-scanner.
Su packageurl.org ci sono gli elenchi completi di strumenti e specifiche che usano il PURL.
VERS: e i range di versioni?
Un PURL indica un pacchetto e, al massimo, una sua versione. Un advisory di sicurezza però non dice quasi mai “è vulnerabile la 2.14.1”: dice “sono vulnerabili tutte le versioni da X a Y”. E ogni fonte scrive i range a modo suo. Per Log4Shell, la pagina di sicurezza di Apache usa la notazione matematica, OSV una lista di eventi introduced/fixed, e ogni package manager ha la sua sintassi ([2.0,2.15) in Maven, >=2.0,<2.15 in pypi, ^2.0.0 in npm…).
VERS (VErsion Range Specifier) è la specifica sorella del PURL, nata nello stesso progetto, che dà a tutti i range la stessa forma:
vers:<type>/<constraint>|<constraint>|...
Lo scheme è sempre vers e il type di solito è lo stesso del PURL. Poi viene una sequenza di constraint separati da |: ogni constraint è una versione da sola (1.2.3, cioè “uguale a”) oppure un comparator seguito da una versione (>=2.0.0). I comparator sono !=, <, <=, >, >=, più *, che da solo vuol dire “tutte le versioni”.
Le versioni di log4j-core colpite da Log4Shell non sono un range unico ma tre, perché Apache ha corretto anche i branch più vecchi: la 2.3.1 per Java 6, la 2.12.2 per Java 7 e la 2.15.0 per Java 8. In VERS:
vers:maven/>=2.0-beta9|<2.3.1|>=2.4|<2.12.2|>=2.13.0|<2.15.0
La specifica chiede che i constraint siano ordinati per versione, che ogni versione compaia una sola volta e che > e < si alternino, così un range vuoto o impossibile si riconosce subito. Nell’esempio di Log4Shell i constraint si leggono a coppie: >=2.4|<2.12.2 è un intervallo, e una versione è nel range se cade in almeno uno degli intervalli.
Un VERS si può inserire in un PURL, con il qualifier vers al posto della versione, per indicare “questo pacchetto, in queste versioni”:
pkg:maven/org.apache.logging.log4j/log4j-core?vers=vers:maven%2F%3E%3D2.0-beta9%7C%3C2.3.1%7C%3E%3D2.4%7C%3C2.12.2%7C%3E%3D2.13.0%7C%3C2.15.0
Scrivere un range però non basta. Per sapere se una versione ci rientra bisogna confrontare due versioni. 2.0-beta9 viene prima o dopo 2.0? E 1.10 rispetto a 1.9? Ogni ecosistema ha le sue regole, e VERS le lascia alla definizione di ogni tipo.
Come il PURL, anche VERS è in corso di standardizzazione da parte del comitato TC54 di Ecma.
Librerie e strumenti
Ci sono librerie per gestire i PURL nei principali linguaggi (Python, Java, Go, JavaScript, .NET, Rust, PHP, Ruby, Swift…), quasi tutte nell’organizzazione package-url su GitHub.
Per il mondo Perl ho creato URI-PackageURL (dal 2025 ne esiste un fork nell’organizzazione package-url), una distribuzione CPAN che genera stringhe PURL valide, ne fa il parsing e supporta anche VERS:
| |
La distribuzione include due comandi da terminale. purl-tool fa il parsing di una stringa PURL o la genera dai componenti, la valida e ne ricava l’URL per scaricare il pacchetto:
| |
[ INFO ]
Il download URL non è salvato da nessuna parte: purl-tool lo ricava dai componenti del PURL. È quello che la specifica intende quando dice che un PURL è anche un locator.
L’ultimo PURL non è valido perché per il tipo maven il namespace (il groupId) è obbligatorio.
vers-tool fa lo stesso per VERS. Per esempio, con il range di Log4Shell visto prima:
$ vers-tool 'vers:maven/>=2.0-beta9|<2.3.1|>=2.4|<2.12.2|>=2.13.0|<2.15.0' --contains 2.14.1
WARNING: Loaded the fallback scheme class comparator.
The comparison may not work correctly!
true
[ WARN ]
Per Maven non esiste ancora una definizione ufficiale del confronto tra versioni, quindi vers-tool usa un comparator generico, che qui funziona ma non è garantito.
Per fare in modo che implementazioni in linguaggi diversi diano lo stesso risultato, purl-spec mantiene una test suite condivisa, indipendente dal linguaggio: un file JSON per le regole generali e uno per ogni tipo registrato (ad oggi sono più di 40), con test di parse, build e validate. Tra il 2025 e il 2026 la test suite è cresciuta molto; nello stesso periodo sono cambiate alcune regole di encoding e decoding, diversi tipi sono stati corretti o migliorati e ne sono arrivati di nuovi. La distribuzione URI-PackageURL usa questi file per i propri test (t/99-*.t) e, a forza di usarli, è diventata un banco di prova per la specifica: diversi errori nei test e nei tipi sono emersi da lì, con issue e PR su purl-spec.
Il tipo cpan
Tra i tipi registrati del PURL c’è anche cpan, e qui la storia diventa un po’ personale.
Durante lo sviluppo iniziale di URI-PackageURL avevo abbozzato, senza volerlo, la prima definizione del tipo cpan. La discussione è partita a novembre 2023 nella issue #8 di URI-PackageURL, diventata poi una specie di gruppo di discussione. Spinto da Salve J. Nilsen di CPANSec, a febbraio 2024 ho aperto su purl-spec una PR per aggiungere cpan ai tipi registrati. È stata accettata due giorni dopo, in pieno FOSDEM, dove Salve ha incontrato Philippe e ha interceduto per me.
Con il tempo la definizione è migliorata e sono state corrette alcune ambiguità: la più importante è lo spostamento dell’autore dal namespace al qualifier author (PR #827, luglio 2026), che spiego più sotto. La nuova definizione dovrebbe permettere un’adozione più ampia nell’ecosistema Perl, grazie a nuovi strumenti come SBOM::CycloneDX e App::CPAN::SBOM (che ho scritto io) e Dist::Zilla::Plugin::CycloneDX.
Nei metadati delle distribuzioni (META.json) il PURL non ha ancora un campo ufficiale, quindi nelle mie distribuzioni rilasciate dal 2024 in poi lo metto nel campo non standard x_purl. L’obiettivo è portarlo prima o poi come campo ufficiale nella CPAN::Meta::Spec. Se ne parla regolarmente nelle riunioni di CPANSec, anche perché, per far funzionare il Cyber Resilience Act con CPAN, i metadati devono descrivere tutto il grafo delle dipendenze.
CPANSec, il gruppo che si occupa della sicurezza di Perl e CPAN, dal 2025 è anche CNA: assegna le CVE per Perl e per le distribuzioni CPAN, 47 nel primo anno. Nei propri record CVE presto inizierà a usare il tipo cpan per indicare il pacchetto coinvolto, grazie al campo packageURL introdotto con il CVE Record Format 5.2.0.
Parliamo un po’ della specifica. Il name è il nome della distribuzione (case-sensitive, e sono vietati i caratteri :: utilizzati per indicare il modulo), e l’autore, quando serve, va nel qualifier author:
pkg:cpan/DBI@1.646
pkg:cpan/URI-PackageURL?author=GDT
pkg:cpan/libwww-perl@6.76?author=OALDERS
pkg:cpan/DateTime@1.55?author=DROLSKY&repository_url=backpan.perl.org
Chi conosce PAUSE e CPAN sa che ogni distribuzione è pubblicata sotto il suo autore (authors/id/G/GD/GDT/URI-PackageURL-2.25.tar.gz). Nel PURL invece l’autore non fa parte del nome, perché una distribuzione può passare da un autore all’altro nel corso della sua vita: con l’autore nel namespace, DBI 1.643 (pubblicata da TIMB) e DBI 1.644 (pubblicata da HMBRAND) avevano PURL diversi pur essendo lo stesso pacchetto. Il namespace (il PAUSE ID in maiuscolo) resta ammesso solo per compatibilità con gli strumenti che già lo usano. URI-PackageURL supporterà la nuova forma dalla prossima release.
Oltre ad author, gli altri qualifier previsti sono repository_url (CPAN, MetaCPAN, BackPAN o un DarkPAN), download_url, vcs_url, ext (l’estensione dell’archivio, tar.gz se manca) e distpath, il percorso dell’archivio dentro il repository (authors/id/...).
E Log4Shell?
A quasi cinque anni dalla scoperta, Log4Shell viene ancora sfruttato. È entrato fin da subito nel catalogo KEV della CISA, ovvero l’elenco delle vulnerabilità sfruttate attivamente, risulta usato in campagne ransomware, e a distanza di anni si trovano ancora prodotti vulnerabili.
Qualcosa però è cambiato: molte aziende hanno cominciato a chiedersi di cosa è fatto il software che usano, e SBOM e PURL si sono diffusi molto di più.
Cosa è successo in quei giorni di dicembre 2021, e perché Log4Shell è ancora in giro, lo racconterò in una delle prossime issue.
Riferimenti
PURL
- packageurl.org: il sito del progetto, con documentazione, tipi registrati, strumenti e specifiche che lo usano
- package-url/purl-spec: la specifica su GitHub, con le definizioni dei tipi e la test suite
- ECMA-427: lo standard (pagina Ecma, comitato TC54)
- Scheme
pkgsu IANA: la registrazione provvisoria - Tipo
cpan(definizione JSON)
VERS
Standard e formati che usano il PURL
- CycloneDX (ECMA-424) e SPDX
- CSAF (specifica 2.0)
- CVE Record Format v5.2.0 (cve-schema)
- OSV (schema)
- CPE, per il confronto
Perl
- CPANSec Retrospective 2025: il primo anno da CNA, PURL e SBOM nell’ecosistema Perl
- CPANSec è CNA per Perl e CPAN, l’annuncio (febbraio 2025)
- URI-PackageURL su MetaCPAN (GitHub), con i comandi purl-tool e vers-tool
Log4Shell
- CVE-2021-44228 su NVD
- Review of the December 2021 Log4j Event, Cyber Safety Review Board (luglio 2022)
SBOM e normative
- Executive Order 14028, Improving the Nation’s Cybersecurity (maggio 2021)
- The Minimum Elements For a Software Bill of Materials, NTIA (luglio 2021)
- Cyber Resilience Act, Regolamento UE 2024/2847 (pagina della Commissione)
>> /dev/gdt
$ mail -s "Re: #001" info@giterlizzi.dev
Correzioni, domande, qualcosa da aggiungere? Scrivimi.