Descrizione
L'autenticazione mediante Spid e CIE è basata sullo standard SAML2 il quale prevede due entità:
- Identity Provider (IdP): il sistema che autentica gli utenti e fornisce informazioni di identità (assertion SAML) al Service Provider, in sostanza, è responsabile della gestione delle credenziali e dell'identità degli utenti;
- Service Provider (SP): il sistema che fornisce un servizio all'utente e si affida all'Identity Provider per autenticare l'utente, riceve le assertion SAML dall'IdP per concedere l'accesso alle risorse.
La libreria cie-aspnetcore è riferita alla seconda entità, ovvero il SP, e implement le logiche di validazione delle assertion SAML presenti all'interno delle SAML response. Segue uno schema riassuntivo di un flusso di autenticazione via SAML:

Come mostrato nello schema, l'IdP, dopo aver verificato le credenziali dell'utente, genera una risposta SAML firmata, questa viene propagata al SP dal browser dell'utente e il SP, dopo aver verificato la firma può estrarre i dati che servono per costruire la sessione dell'utente.
La logica di validazione della firma è centrale in quanto garantisce che non si possa creare una risposta SAML con assertion arbitrarie e quindi impersonare altri utenti.
Segue il codice della validazione implementata in cie-aspnetcore.
internal static bool VerifySignature(XmlDocument signedDocument, IdentityProvider? identityProvider = null){
//...SNIP...
SignedXml signedXml = new SignedXml(signedDocument);
if (identityProvider is not null)
{
bool validated = false;
foreach (var certificate in identityProvider.X509SigningCertificates){
var publicMetadataCert = new X509Certificate2(Convert.FromBase64String(certificate));
XmlNodeList nodeList = (signedDocument.GetElementsByTagName("ds:Signature")?.Count > 1) ?
signedDocument.GetElementsByTagName("ds:Signature") :
(signedDocument.GetElementsByTagName("ns2:Signature")?.Count > 1) ?
signedDocument.GetElementsByTagName("ns2:Signature") :
signedDocument.GetElementsByTagName("Signature");
signedXml.LoadXml((XmlElement)nodeList[0]);
validated |= signedXml.CheckSignature(publicMetadataCert, true);
}
return validated;
}
else{
XmlNodeList nodeList = (signedDocument.GetElementsByTagName("ds:Signature")?.Count > 0) ?
signedDocument.GetElementsByTagName("ds:Signature") :
signedDocument.GetElementsByTagName("Signature");
signedXml.LoadXml((XmlElement)nodeList[0]);
return signedXml.CheckSignature();
}
//...SNIP...
}
Il parametro signedDocument contiene la risposta SAML in formato XML, mentre il parametro identityProvider può contenere le info dell'IdP. Nel caso il parametro identityProvider sia stato specificato vengono estratti i certificati pubblici di tale IdP vengono estratti, così da forzare l'uso degli stessi durante la verifica della firma, altrimenti vengono usati i certificati configurati all'interno dell'applicazione.
Successivamente, viene generata una nodeList all'interno della quale vengono salvati tutti gli elementi XML contenenti una firma XML di parte o di tutto l'envelope della risposta SAML.
Infine, viene estratto il primo elemento di questa lista, ovvero la prima firma trovata, e viene verificata.
In un normale flusso di autenticazione, la risposta SAML simile alla seguente (si noti che sono stati omessi alcuni campi e attributi per semplificare la lettura):
<samlp:Response ID="response_id" IssueInstant="2025-01-07T13:37:00Z" Version="2.0" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">
<saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">
https://demo.spid.gov.it/validator
</saml:Issuer>
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:SignedInfo>
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<ds:Reference URI="#response_id">
<ds:Transforms>
<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
</ds:Transforms>
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<ds:DigestValue>
<!-- DIGEST -->
</ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue>
<!-- SIGNATURE -->
</ds:SignatureValue>
<ds:KeyInfo>
<ds:X509Data>
<ds:X509Certificate>
<!-- CERTIFICATE -->
</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</ds:Signature>
<samlp:Status>
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
</samlp:Status>
<saml:Assertion ID="assertion_id" IssueInstant="2025-01-07T13:37:00Z" Version="2.0" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">
https://demo.spid.gov.it/validator
</saml:Issuer>
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:SignedInfo>
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<ds:Reference URI="#assertion_id">
<ds:Transforms>
<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
</ds:Transforms>
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<ds:DigestValue>
<!-- DIGEST -->
</ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue>
<!-- SIGNATURE -->
</ds:SignatureValue>
<ds:KeyInfo>
<ds:X509Data>
<ds:X509Certificate>
<!-- CERTIFICATE -->
</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</ds:Signature>
<saml:AttributeStatement>
<saml:Attribute Name="spidCode" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
<saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">
AGID-001
</saml:AttributeValue>
</saml:Attribute>
<!-- ... SNIP ... -->
</saml:AttributeStatement>
</saml:Assertion>
</samlp:Response>
Il codice dell'SDK otterrebbe come primo elemento della nodeList, ovvero nodeList[0], la firma riferita all'intera risposta SAML, infatti il riferimento della prima firma <ds:Reference URI="#response_id"> punta al root object <samlp:Response ID="response_id" ...>. Pertanto, verificando questa firma si avrà la certezza che l'intero contenuto della risposta SAML sia integro e autentico.
Tuttavia, non esiste una garanzia che la prima firma faccia riferimento al root object, ne consegue che se un attaccante inietta un elemento firmato come primo elemento, tutte le altre firme non saranno verificate. L'unico requisito è possedere un elemento XML firmato in modo legittimo dall'IdP, condizione facilmente soddisfabile utilizzando i metadata pubblici dell'IdP.
La risposta SAML sarebbe così strutturata:

Impatto
Un attaccante potrebbe creare una risposta SAML arbitraria che verrebbe accettata dai SP che utilizzano gli SDK vulnerabili, consentendogli di impersonare qualsiasi utilizzatore di Spid e/o CIE.
Complessità dell'attacco
L'attaccante necessità di un blocco XML contente una firma valida da parte di uno degli IdP accettati dal SP. Come descritto in precedenza, questo requisito è soddisfacibile leggendo i metadati pubblici dell'IdP che sono rappresentati da un blocco XML firmato dell'IdP.
Problematiche correlate
N/A
PoC
Nota bene: il PoC si riferisce a spid-aspnetcore che condivide le medesime logiche di verifica delle risposte SAML.
- Clonare il repository https://github.com/italia/spid-aspnetcore.git
- Dalla root del progetto, entrare nella cartella relativa alla webapp di esempio:
samples/1_SimpleSPWebApp/SPID.AspNetCore.WebApp/
- Modificare all'interno del file
appsettings.json il valore della chiave AssertionConsumerServiceURL inserendo un dominio personalizzato: https://$CUSTOM_DOMAIN:$CUSTOM_PORT/signin-spid
- Compilare ed eseguire la webapp di esempio tramite il seguente comando, avendo cura di sostituire i placeholder con gli stessi valori utilizzati al punto 3:
dotnet build "SPID.AspNetCore.WebApp.csproj" -o ./app/build && dotnet publish "SPID.AspNetCore.WebApp.csproj" -o ./app/publish && dotnet ./app/publish/SPID.AspNetCore.WebApp.dll -urls=https://$CUSTOM_DOMAIN:$CUSTOM_PORT
- Visitare l'URL:
https://$CUSTOM_DOMAIN:$CUSTOM_PORT/
- Cliccare "Entra con SPID" > "DemoSpid" (secondo IdP della lista)
- Visitare la sezione "Response" > "Check Response"
- Inserire all'interno del campo "Audience" (colonna di destra) la seguente stringa:
https://spid.aspnetcore.it/
- Cliccare "Invia response al Service Provider", notare il redirect verso
/home/loggedin e di conseguenza la corretta esecuzione del login sul portale d'esempio

- Replicare i punti dal 5 all'8 compresi
- Intercettare la richiesta HTTP generata al punto 8 tramite un HTTP Proxy, ad esempio BurpSuite di PortSwigger
- Eseguire URL-decoding e Base64-decoding del parametro POST
SAMLResponse
- Inserire alla seconda riga dell'XML il contenuto presente al seguente URL: https://demo.spid.gov.it/metadata.xml
- Modificare il contenuto del tag
<saml:Assertion>, ad esempio modificare l'attributo email inserendo un valore arbitrario: [email protected]
- Eseguire Base64-encoding e successivamente URL-encoding del parametro
SAMLResponse
- Inviare la richiesta e notare il redirect verso
/home/loggedin il quale dimostra la corretta identificazione e quindi anche la verifica della firma arbitraria inserita nella SAMLResponse nonostante la modifica dell'assertion

Soluzione Consigliata
Verificare tutte le firme presenti all'interno della risposta SAML e non accettare elementi XML non firmati.
Riferimenti
Crediti
Descrizione
L'autenticazione mediante Spid e CIE è basata sullo standard SAML2 il quale prevede due entità:
La libreria
cie-aspnetcoreè riferita alla seconda entità, ovvero il SP, e implement le logiche di validazione delle assertion SAML presenti all'interno delle SAML response. Segue uno schema riassuntivo di un flusso di autenticazione via SAML:Come mostrato nello schema, l'IdP, dopo aver verificato le credenziali dell'utente, genera una risposta SAML firmata, questa viene propagata al SP dal browser dell'utente e il SP, dopo aver verificato la firma può estrarre i dati che servono per costruire la sessione dell'utente.
La logica di validazione della firma è centrale in quanto garantisce che non si possa creare una risposta SAML con assertion arbitrarie e quindi impersonare altri utenti.
Segue il codice della validazione implementata in
cie-aspnetcore.Il parametro
signedDocumentcontiene la risposta SAML in formato XML, mentre il parametroidentityProviderpuò contenere le info dell'IdP. Nel caso il parametroidentityProvidersia stato specificato vengono estratti i certificati pubblici di tale IdP vengono estratti, così da forzare l'uso degli stessi durante la verifica della firma, altrimenti vengono usati i certificati configurati all'interno dell'applicazione.Successivamente, viene generata una
nodeListall'interno della quale vengono salvati tutti gli elementi XML contenenti una firma XML di parte o di tutto l'envelope della risposta SAML.Infine, viene estratto il primo elemento di questa lista, ovvero la prima firma trovata, e viene verificata.
In un normale flusso di autenticazione, la risposta SAML simile alla seguente (si noti che sono stati omessi alcuni campi e attributi per semplificare la lettura):
Il codice dell'SDK otterrebbe come primo elemento della
nodeList, ovveronodeList[0], la firma riferita all'intera risposta SAML, infatti il riferimento della prima firma<ds:Reference URI="#response_id">punta al root object<samlp:Response ID="response_id" ...>. Pertanto, verificando questa firma si avrà la certezza che l'intero contenuto della risposta SAML sia integro e autentico.Tuttavia, non esiste una garanzia che la prima firma faccia riferimento al root object, ne consegue che se un attaccante inietta un elemento firmato come primo elemento, tutte le altre firme non saranno verificate. L'unico requisito è possedere un elemento XML firmato in modo legittimo dall'IdP, condizione facilmente soddisfabile utilizzando i metadata pubblici dell'IdP.
La risposta SAML sarebbe così strutturata:
Impatto
Un attaccante potrebbe creare una risposta SAML arbitraria che verrebbe accettata dai SP che utilizzano gli SDK vulnerabili, consentendogli di impersonare qualsiasi utilizzatore di Spid e/o CIE.
Complessità dell'attacco
L'attaccante necessità di un blocco XML contente una firma valida da parte di uno degli IdP accettati dal SP. Come descritto in precedenza, questo requisito è soddisfacibile leggendo i metadati pubblici dell'IdP che sono rappresentati da un blocco XML firmato dell'IdP.
Problematiche correlate
N/A
PoC
Nota bene: il PoC si riferisce a
spid-aspnetcoreche condivide le medesime logiche di verifica delle risposte SAML.samples/1_SimpleSPWebApp/SPID.AspNetCore.WebApp/appsettings.jsonil valore della chiaveAssertionConsumerServiceURLinserendo un dominio personalizzato:https://$CUSTOM_DOMAIN:$CUSTOM_PORT/signin-spiddotnet build "SPID.AspNetCore.WebApp.csproj" -o ./app/build && dotnet publish "SPID.AspNetCore.WebApp.csproj" -o ./app/publish && dotnet ./app/publish/SPID.AspNetCore.WebApp.dll -urls=https://$CUSTOM_DOMAIN:$CUSTOM_PORThttps://$CUSTOM_DOMAIN:$CUSTOM_PORT/https://spid.aspnetcore.it//home/loggedine di conseguenza la corretta esecuzione del login sul portale d'esempioSAMLResponse<saml:Assertion>, ad esempio modificare l'attributoemailinserendo un valore arbitrario:[email protected]SAMLResponse/home/loggedinil quale dimostra la corretta identificazione e quindi anche la verifica della firma arbitraria inserita nellaSAMLResponsenonostante la modifica dell'assertionSoluzione Consigliata
Verificare tutte le firme presenti all'interno della risposta SAML e non accettare elementi XML non firmati.
Riferimenti
Crediti
smauryOisfi di ShielderpaupuCavaglià di ShielderfromveekoDavico di Shielder