From da9fe4d2d5b71188d8f960ff354ce52ae2925108 Mon Sep 17 00:00:00 2001 From: Andris Reinman Date: Thu, 14 Jul 2022 11:16:36 +0300 Subject: [PATCH 1/2] validate SVG file for VMC --- cli.md | 49 ++++++++++++++++---- lib/bimi/index.js | 34 ++++++++++++++ lib/bimi/validate-svg.js | 96 ++++++++++++++++++++++++++++++++++++++++ lib/mailauth.js | 8 +++- package.json | 5 ++- 5 files changed, 180 insertions(+), 12 deletions(-) create mode 100644 lib/bimi/validate-svg.js diff --git a/cli.md b/cli.md index afa6c80..6eeba1e 100644 --- a/cli.md +++ b/cli.md @@ -238,9 +238,6 @@ $ mailauth vmc -a https://amplify.valimail.com/bimi/time-warner/yV3KRIg4nJW-cnn. "logoFile": "<2300B base64 encoded file>", "validHash": true, "certificate": { - "subjectAltName": [ - "cnn.com" - ], "subject": { "businessCategory": "Private Organization", "jurisdictionCountryName": "US", @@ -255,8 +252,13 @@ $ mailauth vmc -a https://amplify.valimail.com/bimi/time-warner/yV3KRIg4nJW-cnn. "trademarkCountryOrRegionName": "US", "trademarkRegistration": "5817930" }, + "subjectAltName": [ + "cnn.com" + ], "fingerprint": "17:B3:94:97:E6:6B:C8:6B:33:B8:0A:D2:F0:79:6B:08:A2:A6:84:BD", "serialNumber": "0821B8FE0A9CBC3BAC10DA08C088EEF4", + "validFrom": "2021-08-12T00:00:00.000Z", + "validTo": "2022-08-12T23:59:59.000Z", "issuer": { "countryName": "US", "organizationName": "DigiCert, Inc.", @@ -267,7 +269,7 @@ $ mailauth vmc -a https://amplify.valimail.com/bimi/time-warner/yV3KRIg4nJW-cnn. } ``` -If the certificate verification fails, then the contents are not returned. +If the certificate verification fails, then the logo contents are not returned. ``` $ mailauth vmc -p /path/to/random/cert-bundle.pem @@ -276,17 +278,46 @@ $ mailauth vmc -p /path/to/random/cert-bundle.pem "error": { "message": "Self signed certificate in certificate chain", "details": { - "subject": "CN=postal.vmc.local\nO=Postal Systems OU.\nC=EE", - "fingerprint": "CC:49:83:ED:3F:6B:77:45:5B:A5:3B:9E:EC:99:0E:A1:EF:D7:FF:97", - "fingerprint235": "D4:36:6F:B4:EF:2B:4F:9E:84:23:3D:F2:3A:F7:13:21:C6:C3:CF:CB:03:5F:BB:54:5B:69:A4:AC:6A:43:61:7D", - "validFrom": "2022-07-10T06:28:06.482Z", - "validTo": "2022-07-10T06:28:06.482Z" + "certificate": { + "subject": { + "commonName": "postal.vmc.local", + "organizationName": "Postal Systems OU.", + "countryName": "EE" + }, + "subjectAltName": [], + "fingerprint": "CC:49:83:ED:3F:6B:77:45:5B:A5:3B:9E:EC:99:0E:A1:EF:D7:FF:97", + "serialNumber": "B61FBFBA917B15D9", + "validFrom": "2022-07-09T06:13:33.000Z", + "validTo": "2023-07-09T06:13:33.000Z", + "issuer": { + "commonName": "postal.vmc.local", + "organizationName": "Postal Systems OU.", + "countryName": "EE" + } + } }, "code": "SELF_SIGNED_CERT_IN_CHAIN" } } ``` +The embedded SVG file is also validated. + +``` +$ mailauth vmc -p /path/to/vmc-with-invalid-svg.pem +{ + "success": false, + "error": { + "message": "VMC logo SVG validation failed", + "details": { + "message": "Not a Tiny PS profile", + "code": "INVALID_BASE_PROFILE" + }, + "code": "SVG_VALIDATION_FAILED" + } +} +``` + ### license Display licenses for `mailauth` and included modules. diff --git a/lib/bimi/index.js b/lib/bimi/index.js index a893f8a..be0e9ab 100644 --- a/lib/bimi/index.js +++ b/lib/bimi/index.js @@ -12,6 +12,7 @@ const httpsSchema = Joi.string().uri({ const https = require('https'); const http = require('http'); const { vmc } = require('@postalsys/vmc'); +const { validateSvg } = require('./validate-svg'); const lookup = async data => { let { dmarc, headers, resolver } = data; @@ -303,6 +304,12 @@ const validateVMC = async bimiData => { try { let vmcData = await vmc(authorityValue); + if (!vmcData.logoFile) { + let error = new Error('VMC does not contain a log file'); + error.code = 'MISSING_VMC_LOGO'; + throw error; + } + if (vmcData?.mediaType?.toLowerCase() !== 'image/svg+xml') { let error = new Error('Invalid media type for the logo file'); error.details = { @@ -312,6 +319,33 @@ const validateVMC = async bimiData => { throw error; } + if (!vmcData.validHash) { + let error = new Error('VMC hash does not match logo file'); + error.details = { + hashAlgo: vmcData.hashAlgo, + hashValue: vmcData.hashValue, + logoFile: vmcData.logoFile + }; + error.code = 'INVALID_LOGO_HASH'; + throw error; + } + + // throws on invalid logo file + try { + validateSvg(Buffer.from(vmcData.logoFile, 'base64')); + } catch (err) { + let error = new Error('VMC logo SVG validation failed'); + error.details = Object.assign( + { + message: err.message + }, + error.details || {}, + err.code ? { code: err.code } : {} + ); + error.code = 'SVG_VALIDATION_FAILED'; + throw error; + } + if (d) { // validate domain let selectorSet = []; diff --git a/lib/bimi/validate-svg.js b/lib/bimi/validate-svg.js new file mode 100644 index 0000000..c8c712b --- /dev/null +++ b/lib/bimi/validate-svg.js @@ -0,0 +1,96 @@ +'use strict'; + +const { XMLParser } = require('fast-xml-parser'); + +function validateSvg(logo) { + const parser = new XMLParser({ + ignoreAttributes: false, + attributeNamePrefix: '@_' + }); + + let logoObj; + try { + logoObj = parser.parse(logo); + if (!logoObj) { + throw new Error('Emtpy file'); + } + } catch (err) { + let error = new Error('Invalid SVG file'); + error._err = err; + error.code = 'INVALID_XML_FILE'; + throw error; + } + + if (!logoObj.svg) { + let error = new Error('Invalid SVG file'); + error.code = 'INVALID_SVG_FILE'; + throw error; + } + + if (logoObj.svg['@_baseProfile'] !== 'tiny-ps') { + let error = new Error('Not a Tiny PS profile'); + error.code = 'INVALID_BASE_PROFILE'; + throw error; + } + + if (!logoObj.svg.title) { + let error = new Error('Logo file is missing title'); + error.code = 'LOGO_MISSING_TITLE'; + throw error; + } + + if ('@_x' in logoObj.svg || '@_y' in logoObj.svg) { + let error = new Error('Logo root includes x/y attributes'); + error.code = 'LOGO_INVALID_ROOT_ATTRS'; + throw error; + } + + let walkElm = (node, name, path) => { + if (!node) { + return; + } + if (Array.isArray(node)) { + for (let entry of node) { + walkElm(entry, name, path + '.' + name + '[]'); + } + } else if (typeof node === 'object') { + if (node['@_xlink:href'] && !/^#/.test(node['@_xlink:href'])) { + let error = new Error('External reference found from file'); + error.details = { + element: name, + link: node['@_xlink:href'], + path + }; + error.code = 'LOGO_INCLUDES_REFERENCE'; + throw error; + } + + for (let key of Object.keys(node)) { + if (['script', 'animate', 'animatemotion', 'animatetransform', 'discard', 'set'].includes(key.toLowerCase())) { + let error = new Error('Unallowed element found from file'); + error.details = { + element: key, + path: path + '.' + key + }; + error.code = 'LOGO_INVALID_ELEMENT'; + throw error; + } + + if (Array.isArray(node[key])) { + for (let entry of node[key]) { + walkElm(entry, key, path + '.' + key + '[]'); + } + } else if (node[key] && typeof node[key] === 'object') { + walkElm(node[key], key, path + '.' + key); + } + } + } + }; + + walkElm(logoObj, 'root', ''); + + // all validations passed + return true; +} + +module.exports = { validateSvg }; diff --git a/lib/mailauth.js b/lib/mailauth.js index 14abd91..ad2e8c9 100644 --- a/lib/mailauth.js +++ b/lib/mailauth.js @@ -5,6 +5,7 @@ const { spf } = require('./spf'); const { dmarc } = require('./dmarc'); const { arc, createSeal } = require('./arc'); const { bimi, validateVMC: validateBimiVmc } = require('./bimi'); +const { validateSvg: validateBimiSvg } = require('./bimi/validate-svg'); const { parseReceived } = require('./parse-received'); const { sealMessage } = require('./arc'); const libmime = require('libmime'); @@ -180,4 +181,9 @@ const authenticate = async (input, opts) => { }; }; -module.exports = { authenticate, sealMessage, validateBimiVmc }; +module.exports = { + authenticate, + sealMessage, + validateBimiVmc, + validateBimiSvg +}; diff --git a/package.json b/package.json index 59d230b..6d19b67 100644 --- a/package.json +++ b/package.json @@ -42,10 +42,11 @@ "marked-man": "0.7.0", "mbox-reader": "1.1.5", "mocha": "10.0.0", - "pkg": "5.7.0" + "pkg": "5.8.0" }, "dependencies": { - "@postalsys/vmc": "1.0.3", + "@postalsys/vmc": "1.0.4", + "fast-xml-parser": "4.0.9", "ipaddr.js": "2.0.1", "joi": "17.6.0", "libmime": "5.1.0", From ecb9af42a6545c57f8780056c5acf3682d4f101f Mon Sep 17 00:00:00 2001 From: Andris Reinman Date: Thu, 14 Jul 2022 11:17:25 +0300 Subject: [PATCH 2/2] v3.0.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6d19b67..498bacb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mailauth", - "version": "3.0.1", + "version": "3.0.2", "description": "Email authentication library for Node.js", "main": "lib/mailauth.js", "scripts": {