Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FIX] Wrong SAML Response Signature Validation #16922

Merged
merged 8 commits into from
Apr 8, 2020
28 changes: 17 additions & 11 deletions app/meteor-accounts-saml/server/saml_server.js
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,22 @@ const guessNameFromUsername = (username) =>
.replace(/^(.)/, (u) => u.toLowerCase())
.replace(/^\w/, (u) => u.toUpperCase());

const findUser = (username, emailRegex) => {
if (Accounts.saml.settings.immutableProperty === 'Username') {
if (username) {
return Meteor.users.findOne({
username,
});
}

return null;
}

return Meteor.users.findOne({
'emails.address': emailRegex,
});
};

Accounts.registerLoginHandler(function(loginRequest) {
if (!loginRequest.saml || !loginRequest.credentialToken) {
return undefined;
Expand Down Expand Up @@ -279,17 +295,7 @@ Accounts.registerLoginHandler(function(loginRequest) {

// If eppn is not exist
if (!user) {
if (Accounts.saml.settings.immutableProperty === 'Username') {
if (username) {
user = Meteor.users.findOne({
username,
});
}
} else {
user = Meteor.users.findOne({
'emails.address': emailRegex,
});
}
user = findUser(username, emailRegex);
}

const emails = emailList.map((email) => ({
Expand Down
184 changes: 143 additions & 41 deletions app/meteor-accounts-saml/server/saml_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -311,12 +311,8 @@ SAML.prototype.validateStatus = function(doc) {
};
};

SAML.prototype.validateSignature = function(xml, cert) {
SAML.prototype.validateSignature = function(xml, cert, signature) {
const self = this;

const doc = new xmldom.DOMParser().parseFromString(xml);
const signature = xmlCrypto.xpath(doc, '//*[local-name(.)=\'Signature\' and namespace-uri(.)=\'http://www.w3.org/2000/09/xmldsig#\']')[0];

const sig = new xmlCrypto.SignedXml();

sig.keyInfoProvider = {
Expand All @@ -333,6 +329,35 @@ SAML.prototype.validateSignature = function(xml, cert) {
return sig.checkSignature(xml);
};

SAML.prototype.validateSignatureChildren = function(xml, cert, parent) {
const xpathSigQuery = ".//*[local-name(.)='Signature' and namespace-uri(.)='http://www.w3.org/2000/09/xmldsig#']";
const signatures = xmlCrypto.xpath(parent, xpathSigQuery);
let signature = null;

for (const sign of signatures) {
if (sign.parentNode !== parent) {
continue;
}

// Too many signatures
if (signature) {
return false;
}

signature = sign;
}

return this.validateSignature(xml, cert, signature);
};

SAML.prototype.validateResponseSignature = function(xml, cert, response) {
return this.validateSignatureChildren(xml, cert, response);
};

SAML.prototype.validateAssertionSignature = function(xml, cert, assertion) {
return this.validateSignatureChildren(xml, cert, assertion);
};

SAML.prototype.validateLogoutRequest = function(samlRequest, callback) {
const compressedSAMLRequest = new Buffer(samlRequest, 'base64');
zlib.inflateRaw(compressedSAMLRequest, function(err, decoded) {
Expand Down Expand Up @@ -464,6 +489,23 @@ SAML.prototype.mapAttributes = function(attributeStatement, profile) {
}
};

SAML.prototype.validateAssertionConditions = function(assertion) {
const conditions = assertion.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'Conditions')[0];
if (conditions && !this.validateNotBeforeNotOnOrAfterAssertions(conditions)) {
throw new Error('NotBefore / NotOnOrAfter assertion failed');
}
};

SAML.prototype.validateSubjectConditions = function(subject) {
const subjectConfirmation = subject.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'SubjectConfirmation')[0];
if (subjectConfirmation) {
const subjectConfirmationData = subjectConfirmation.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'SubjectConfirmationData')[0];
if (subjectConfirmationData && !this.validateNotBeforeNotOnOrAfterAssertions(subjectConfirmationData)) {
throw new Error('NotBefore / NotOnOrAfter assertion failed');
}
}
};

SAML.prototype.validateNotBeforeNotOnOrAfterAssertions = function(element) {
const sysnow = new Date();
const allowedclockdrift = this.options.allowedClockDrift;
Expand Down Expand Up @@ -491,6 +533,76 @@ SAML.prototype.validateNotBeforeNotOnOrAfterAssertions = function(element) {
return true;
};

SAML.prototype.getAssertion = function(response) {
const allAssertions = response.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'Assertion');
const allEncrypedAssertions = response.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'EncryptedAssertion');

if (allAssertions.length + allEncrypedAssertions.length > 1) {
throw new Error('Too many SAML assertions');
}

let assertion = allAssertions[0];
const encAssertion = allEncrypedAssertions[0];


if (typeof encAssertion !== 'undefined') {
const options = { key: this.options.privateKey };
xmlenc.decrypt(encAssertion.getElementsByTagNameNS('*', 'EncryptedData')[0], options, function(err, result) {
assertion = new xmldom.DOMParser().parseFromString(result, 'text/xml');
});
}

if (!assertion) {
throw new Error('Missing SAML assertion');
}

return assertion;
};

SAML.prototype.verifySignatures = function(response, assertion, xml) {
if (!this.options.cert) {
return;
}

debugLog('Verify Document Signature');
if (!this.validateResponseSignature(xml, this.options.cert, response)) {
debugLog('Document Signature WRONG');
throw new Error('Invalid Signature');
}
debugLog('Document Signature OK');

debugLog('Verify Assertion Signature');
if (!this.validateAssertionSignature(xml, this.options.cert, assertion)) {
debugLog('Assertion Signature WRONG');
throw new Error('Invalid Assertion signature');
}
debugLog('Assertion Signature OK');
};

SAML.prototype.getSubject = function(assertion) {
let subject = assertion.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'Subject')[0];
const encSubject = assertion.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'EncryptedID')[0];

if (typeof encSubject !== 'undefined') {
const options = { key: this.options.privateKey };
xmlenc.decrypt(encSubject.getElementsByTagNameNS('*', 'EncryptedData')[0], options, function(err, result) {
subject = new xmldom.DOMParser().parseFromString(result, 'text/xml');
});
}

return subject;
};

SAML.prototype.getIssuer = function(assertion) {
const issuers = assertion.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'Issuer');

if (issuers.length > 1) {
throw new Error('Too many Issuers');
}

return issuers[0];
};

SAML.prototype.validateResponse = function(samlResponse, relayState, callback) {
const self = this;
const xml = new Buffer(samlResponse, 'base64').toString('utf8');
Expand All @@ -510,15 +622,12 @@ SAML.prototype.validateResponse = function(samlResponse, relayState, callback) {
}
debugLog('Status ok');

// Verify signature
debugLog('Verify signature');
if (self.options.cert && !self.validateSignature(xml, self.options.cert)) {
debugLog('Signature WRONG');
return callback(new Error('Invalid signature'), null, false);
const allResponses = doc.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:protocol', 'Response');
if (allResponses.length !== 1) {
return callback(new Error('Too many SAML responses'), null, false);
}
debugLog('Signature OK');

const response = doc.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:protocol', 'Response')[0];
const response = allResponses[0];
if (!response) {
const logoutResponse = doc.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:protocol', 'LogoutResponse');

Expand All @@ -529,19 +638,15 @@ SAML.prototype.validateResponse = function(samlResponse, relayState, callback) {
}
debugLog('Got response');

let assertion = response.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'Assertion')[0];
const encAssertion = response.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'EncryptedAssertion')[0];
let assertion;
let issuer;

const options = { key: this.options.privateKey };
try {
assertion = this.getAssertion(response, callback);

if (typeof encAssertion !== 'undefined') {
xmlenc.decrypt(encAssertion.getElementsByTagNameNS('*', 'EncryptedData')[0], options, function(err, result) {
assertion = new xmldom.DOMParser().parseFromString(result, 'text/xml');
});
}

if (!assertion) {
return callback(new Error('Missing SAML assertion'), null, false);
this.verifySignatures(response, assertion, xml);
} catch (e) {
return callback(e, null, false);
}

const profile = {};
Expand All @@ -550,19 +655,17 @@ SAML.prototype.validateResponse = function(samlResponse, relayState, callback) {
profile.inResponseToId = response.getAttribute('InResponseTo');
}

const issuer = assertion.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'Issuer')[0];
try {
issuer = this.getIssuer(assertion);
} catch (e) {
return callback(e, null, false);
}

if (issuer) {
profile.issuer = issuer.textContent;
}

let subject = assertion.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'Subject')[0];
const encSubject = assertion.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'EncryptedID')[0];

if (typeof encSubject !== 'undefined') {
xmlenc.decrypt(encSubject.getElementsByTagNameNS('*', 'EncryptedData')[0], options, function(err, result) {
subject = new xmldom.DOMParser().parseFromString(result, 'text/xml');
});
}
const subject = this.getSubject(assertion);

if (subject) {
const nameID = subject.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'NameID')[0];
Expand All @@ -574,18 +677,17 @@ SAML.prototype.validateResponse = function(samlResponse, relayState, callback) {
}
}

const subjectConfirmation = subject.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'SubjectConfirmation')[0];
if (subjectConfirmation) {
const subjectConfirmationData = subjectConfirmation.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'SubjectConfirmationData')[0];
if (subjectConfirmationData && !this.validateNotBeforeNotOnOrAfterAssertions(subjectConfirmationData)) {
return callback(new Error('NotBefore / NotOnOrAfter assertion failed'), null, false);
}
try {
this.validateSubjectConditions(subject);
} catch (e) {
return callback(e, null, false);
}
}

const conditions = assertion.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'Conditions')[0];
if (conditions && !this.validateNotBeforeNotOnOrAfterAssertions(conditions)) {
return callback(new Error('NotBefore / NotOnOrAfter assertion failed'), null, false);
try {
this.validateAssertionConditions(assertion);
} catch (e) {
return callback(e, null, false);
}

const authnStatement = assertion.getElementsByTagNameNS('urn:oasis:names:tc:SAML:2.0:assertion', 'AuthnStatement')[0];
Expand Down