mailauth 7.1.1 → 7.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.c8rc.json +11 -0
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +53 -0
- package/README.md +32 -31
- package/cli.md +1 -1
- package/docs/arc.md +62 -54
- package/docs/bimi.md +153 -54
- package/docs/dkim.md +37 -36
- package/docs/dmarc.md +4 -1
- package/docs/mta-sts.md +12 -9
- package/docs/spf.md +14 -13
- package/index.d.ts +224 -42
- package/lib/arc/index.d.ts +6 -2
- package/lib/arc/index.js +45 -15
- package/lib/bimi/index.js +362 -102
- package/lib/bimi/validate-svg.js +493 -60
- package/lib/commands/report.js +2 -2
- package/lib/commands/seal.js +8 -6
- package/lib/commands/spf.js +2 -2
- package/lib/dkim/dkim-signer.js +50 -30
- package/lib/dkim/dkim-verifier.js +79 -27
- package/lib/dkim/header/simple.js +2 -2
- package/lib/dkim/syntax.js +11 -0
- package/lib/dkim/verify.js +1 -0
- package/lib/dmarc/author-domain.js +270 -5
- package/lib/dmarc/get-dmarc-record.js +24 -11
- package/lib/dmarc/verify.js +7 -3
- package/lib/mailauth.js +17 -14
- package/lib/mta-sts.js +37 -11
- package/lib/parse-received.js +214 -25
- package/lib/spf/index.js +21 -4
- package/lib/spf/macro.js +5 -3
- package/lib/spf/spf-verify.js +14 -5
- package/lib/spf/syntax.js +5 -0
- package/lib/tools.js +159 -21
- package/package.json +14 -5
- package/stryker.config.json +16 -0
package/lib/bimi/validate-svg.js
CHANGED
|
@@ -1,93 +1,526 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { XMLParser } = require('fast-xml-parser');
|
|
3
|
+
const { XMLParser, XMLValidator } = require('fast-xml-parser');
|
|
4
|
+
const { createError, addWarning } = require('../tools');
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
// Validates a BIMI Indicator against the SVG Tiny Portable/Secure profile (draft-svg-tiny-ps-abrotman).
|
|
7
|
+
// Elements are checked against an allowlist with resolved namespaces. Script, interactivity, linking,
|
|
8
|
+
// multimedia and animation are not allowed (section 2.3), and every reference must point into the
|
|
9
|
+
// document itself. The checks also cover markup that an HTML parser would read differently than an
|
|
10
|
+
// XML parser, as some clients insert the SVG into an HTML document.
|
|
11
|
+
|
|
12
|
+
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
13
|
+
const XML_NS = 'http://www.w3.org/XML/1998/namespace';
|
|
14
|
+
const XMLNS_NS = 'http://www.w3.org/2000/xmlns/';
|
|
15
|
+
// elements in these namespaces are rendered or executed by browsers
|
|
16
|
+
const ACTIVE_NAMESPACES = new Set(['http://www.w3.org/1999/xhtml', 'http://www.w3.org/1998/Math/MathML']);
|
|
17
|
+
|
|
18
|
+
// The element set of the validation RNC schema (section 7)
|
|
19
|
+
const TINY_PS_ELEMENTS = new Set([
|
|
20
|
+
'svg',
|
|
21
|
+
'title',
|
|
22
|
+
'desc',
|
|
23
|
+
'metadata',
|
|
24
|
+
'path',
|
|
25
|
+
'rect',
|
|
26
|
+
'circle',
|
|
27
|
+
'line',
|
|
28
|
+
'ellipse',
|
|
29
|
+
'polyline',
|
|
30
|
+
'polygon',
|
|
31
|
+
'solidColor',
|
|
32
|
+
'textArea',
|
|
33
|
+
'linearGradient',
|
|
34
|
+
'radialGradient',
|
|
35
|
+
'stop',
|
|
36
|
+
'text',
|
|
37
|
+
'g',
|
|
38
|
+
'defs',
|
|
39
|
+
'use',
|
|
40
|
+
'font',
|
|
41
|
+
'glyph',
|
|
42
|
+
'hkern'
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
// Static rendering elements of SVG 1.1 that are not part of the profile, but are found in published
|
|
46
|
+
// logos. They do not run code or load anything, references are checked like everywhere else.
|
|
47
|
+
const STATIC_ELEMENTS = new Set([
|
|
48
|
+
'style',
|
|
49
|
+
'clipPath',
|
|
50
|
+
'mask',
|
|
51
|
+
'pattern',
|
|
52
|
+
'symbol',
|
|
53
|
+
'marker',
|
|
54
|
+
'tspan',
|
|
55
|
+
'tbreak',
|
|
56
|
+
'textPath',
|
|
57
|
+
'missing-glyph',
|
|
58
|
+
'vkern',
|
|
59
|
+
'font-face',
|
|
60
|
+
'font-face-src',
|
|
61
|
+
'font-face-name',
|
|
62
|
+
'filter',
|
|
63
|
+
'feBlend',
|
|
64
|
+
'feColorMatrix',
|
|
65
|
+
'feComponentTransfer',
|
|
66
|
+
'feComposite',
|
|
67
|
+
'feConvolveMatrix',
|
|
68
|
+
'feDiffuseLighting',
|
|
69
|
+
'feDisplacementMap',
|
|
70
|
+
'feDistantLight',
|
|
71
|
+
'feDropShadow',
|
|
72
|
+
'feFlood',
|
|
73
|
+
'feFuncA',
|
|
74
|
+
'feFuncB',
|
|
75
|
+
'feFuncG',
|
|
76
|
+
'feFuncR',
|
|
77
|
+
'feGaussianBlur',
|
|
78
|
+
'feMerge',
|
|
79
|
+
'feMergeNode',
|
|
80
|
+
'feMorphology',
|
|
81
|
+
'feOffset',
|
|
82
|
+
'fePointLight',
|
|
83
|
+
'feSpecularLighting',
|
|
84
|
+
'feSpotLight',
|
|
85
|
+
'feTile',
|
|
86
|
+
'feTurbulence'
|
|
87
|
+
]);
|
|
88
|
+
|
|
89
|
+
const MAX_DEPTH = 256;
|
|
90
|
+
|
|
91
|
+
// Attributes that SHOULD NOT be present, and if present MUST have this value (sections 2.2 and 2.3)
|
|
92
|
+
const FIXED_ATTRIBUTE_VALUES = {
|
|
93
|
+
zoomAndPan: 'disable',
|
|
94
|
+
externalResourcesRequired: 'false',
|
|
95
|
+
focusable: 'false',
|
|
96
|
+
snapshotTime: 'none',
|
|
97
|
+
playbackOrder: 'all',
|
|
98
|
+
timelineBegin: 'onLoad',
|
|
99
|
+
editable: 'none'
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const PREDEFINED_ENTITIES = { lt: '<', gt: '>', amp: '&', quot: '"', apos: "'" };
|
|
103
|
+
|
|
104
|
+
const parser = new XMLParser({
|
|
105
|
+
ignoreAttributes: false,
|
|
106
|
+
attributeNamePrefix: '',
|
|
107
|
+
preserveOrder: true,
|
|
108
|
+
// entities are decoded by decodeEntities(), the parser would leave some character references as is
|
|
109
|
+
processEntities: false,
|
|
110
|
+
htmlEntities: false,
|
|
111
|
+
parseTagValue: false,
|
|
112
|
+
parseAttributeValue: false,
|
|
113
|
+
trimValues: false,
|
|
114
|
+
ignoreDeclaration: false,
|
|
115
|
+
ignorePiTags: false,
|
|
116
|
+
commentPropName: '#comment',
|
|
117
|
+
cdataPropName: '#cdata',
|
|
118
|
+
textNodeName: '#text'
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const invalidXml = (message, details) => createError('Invalid SVG file', 'INVALID_XML_FILE', { details: Object.assign({ message }, details || {}) });
|
|
122
|
+
|
|
123
|
+
// a link or a CSS value that could load a resource from outside the document
|
|
124
|
+
const externalReference = details => createError('External reference found from file', 'LOGO_INCLUDES_REFERENCE', { details });
|
|
125
|
+
|
|
126
|
+
const isXmlChar = code =>
|
|
127
|
+
code === 0x09 ||
|
|
128
|
+
code === 0x0a ||
|
|
129
|
+
code === 0x0d ||
|
|
130
|
+
(code >= 0x20 && code <= 0xd7ff) ||
|
|
131
|
+
(code >= 0xe000 && code <= 0xfffd) ||
|
|
132
|
+
(code >= 0x10000 && code <= 0x10ffff);
|
|
133
|
+
|
|
134
|
+
// Decodes the predefined entities and character references. Other entities are only defined in a
|
|
135
|
+
// DTD, which is not allowed.
|
|
136
|
+
const decodeEntities = value =>
|
|
137
|
+
value.replace(/&([^;&]*);?/g, (match, ref) => {
|
|
138
|
+
if (match.charAt(match.length - 1) !== ';') {
|
|
139
|
+
throw invalidXml('Unterminated entity reference');
|
|
140
|
+
}
|
|
141
|
+
if (Object.prototype.hasOwnProperty.call(PREDEFINED_ENTITIES, ref)) {
|
|
142
|
+
return PREDEFINED_ENTITIES[ref];
|
|
143
|
+
}
|
|
144
|
+
let code = /^#x[0-9a-fA-F]+$/.test(ref) ? parseInt(ref.slice(2), 16) : /^#[0-9]+$/.test(ref) ? parseInt(ref.slice(1), 10) : NaN;
|
|
145
|
+
if (!isXmlChar(code)) {
|
|
146
|
+
throw invalidXml(`Invalid entity reference &${ref};`);
|
|
147
|
+
}
|
|
148
|
+
return String.fromCodePoint(code);
|
|
9
149
|
});
|
|
10
150
|
|
|
151
|
+
// Returns the reason why a CSS value or a presentation attribute might load an external resource
|
|
152
|
+
const checkCss = value => {
|
|
153
|
+
if (value.indexOf('\\') >= 0) {
|
|
154
|
+
// escapes could hide function names and URLs from the checks below
|
|
155
|
+
return 'CSS escape';
|
|
156
|
+
}
|
|
157
|
+
if (/@import/i.test(value)) {
|
|
158
|
+
return 'CSS import';
|
|
159
|
+
}
|
|
160
|
+
if (/(?:src|image|image-set|cross-fade|element)\s*\(/i.test(value)) {
|
|
161
|
+
return 'CSS image function';
|
|
162
|
+
}
|
|
163
|
+
let urlRe = /url\s*\(\s*(['"]?)\s*([^'")]*)/gi;
|
|
164
|
+
let match;
|
|
165
|
+
while ((match = urlRe.exec(value))) {
|
|
166
|
+
if (match[2].charAt(0) !== '#') {
|
|
167
|
+
return 'external url()';
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return false;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const splitName = name => {
|
|
174
|
+
let pos = name.indexOf(':');
|
|
175
|
+
return pos >= 0 ? { prefix: name.slice(0, pos), local: name.slice(pos + 1) } : { prefix: '', local: name };
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Validates an SVG Indicator against the SVG Tiny PS profile
|
|
180
|
+
*
|
|
181
|
+
* @param {Buffer|String} logo SVG document
|
|
182
|
+
* @param {Object} [opts]
|
|
183
|
+
* @param {Boolean} [opts.strict=false] Apply the profile exactly: the version attribute, a single title as
|
|
184
|
+
* the first child, only the elements of the validation schema and the required attribute values
|
|
185
|
+
* @param {Array} [opts.warnings] Without strict, markers of the profile deviations that were accepted are added here
|
|
186
|
+
* @returns {Boolean} true, throws if the document is not valid
|
|
187
|
+
*/
|
|
188
|
+
function validateSvg(logo, opts) {
|
|
189
|
+
opts = opts || {};
|
|
190
|
+
const strict = !!opts.strict;
|
|
191
|
+
const warnings = Array.isArray(opts.warnings) ? opts.warnings : [];
|
|
192
|
+
|
|
193
|
+
// a deviation from the profile that does not make the logo unsafe, an error only in strict mode
|
|
194
|
+
const lax = (marker, error) => {
|
|
195
|
+
if (strict) {
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
198
|
+
addWarning(warnings, marker);
|
|
199
|
+
};
|
|
200
|
+
let source = Buffer.isBuffer(logo) ? logo.toString('utf-8') : String(logo || '');
|
|
201
|
+
if (source.charCodeAt(0) === 0xfeff) {
|
|
202
|
+
source = source.slice(1);
|
|
203
|
+
}
|
|
204
|
+
|
|
11
205
|
let logoObj;
|
|
12
206
|
try {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
207
|
+
if (!source.trim()) {
|
|
208
|
+
throw new Error('Empty file');
|
|
209
|
+
}
|
|
210
|
+
logoObj = parser.parse(source);
|
|
211
|
+
if (!Array.isArray(logoObj)) {
|
|
212
|
+
throw new Error('Empty file');
|
|
16
213
|
}
|
|
17
214
|
} catch (err) {
|
|
18
|
-
|
|
19
|
-
error._err = err;
|
|
20
|
-
error.code = 'INVALID_XML_FILE';
|
|
21
|
-
throw error;
|
|
215
|
+
throw createError('Invalid SVG file', 'INVALID_XML_FILE', { _err: err });
|
|
22
216
|
}
|
|
23
217
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
218
|
+
const nodeName = node => Object.keys(node).find(key => key !== ':@');
|
|
219
|
+
|
|
220
|
+
let root = logoObj.find(node => /^[^#?]/.test(nodeName(node) || ''));
|
|
221
|
+
if (!root || splitName(nodeName(root)).local !== 'svg') {
|
|
222
|
+
throw createError('Invalid SVG file', 'INVALID_SVG_FILE');
|
|
28
223
|
}
|
|
29
224
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
throw
|
|
225
|
+
// the parser accepts mismatched tags, repeated attributes and similar errors, the validator does not
|
|
226
|
+
let validation = XMLValidator.validate(source);
|
|
227
|
+
if (validation !== true) {
|
|
228
|
+
throw invalidXml(validation?.err?.msg || 'Invalid XML');
|
|
34
229
|
}
|
|
35
230
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
231
|
+
const childText = node => (node[nodeName(node)] || []).map(entry => entry['#text'] || '').join('');
|
|
232
|
+
|
|
233
|
+
const checkComment = (node, path) => {
|
|
234
|
+
// "<!-->" and "--!>" end a comment in HTML
|
|
235
|
+
let text = childText(node);
|
|
236
|
+
if (/[<>]|--/.test(text) || /^-|-$/.test(text)) {
|
|
237
|
+
throw createError('Unexpected content in comment', 'LOGO_INVALID_CONTENT', { details: { path } });
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
// An internal DTD subset can declare entities and default attributes, the parser and a browser
|
|
242
|
+
// would not see the same document. The DOCTYPE of SVG 1.1 is allowed, it is ignored by browsers.
|
|
243
|
+
let doctypeRe = /<!doctype/gi;
|
|
244
|
+
let doctypeMatch;
|
|
245
|
+
while ((doctypeMatch = doctypeRe.exec(source))) {
|
|
246
|
+
let tail = source.slice(doctypeMatch.index);
|
|
247
|
+
if (!/^<!DOCTYPE\s+svg(?:\s+PUBLIC\s+"[^"<>[\]]*"\s+"[^"<>[\]]*"|\s+SYSTEM\s+"[^"<>[\]]*")?\s*>/.test(tail)) {
|
|
248
|
+
throw invalidXml('Unsupported DOCTYPE declaration');
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (/<!ENTITY/i.test(source)) {
|
|
252
|
+
throw invalidXml('Entity declarations are not allowed');
|
|
40
253
|
}
|
|
41
254
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
255
|
+
let rootCount = 0;
|
|
256
|
+
for (let [i, node] of logoObj.entries()) {
|
|
257
|
+
let name = nodeName(node);
|
|
258
|
+
if (name === '#text') {
|
|
259
|
+
if ((node['#text'] || '').trim()) {
|
|
260
|
+
throw invalidXml('Text outside of the root element');
|
|
261
|
+
}
|
|
262
|
+
} else if (name === '#comment') {
|
|
263
|
+
checkComment(node, '');
|
|
264
|
+
} else if (name === '?xml') {
|
|
265
|
+
if (i !== 0) {
|
|
266
|
+
throw invalidXml('Misplaced XML declaration');
|
|
267
|
+
}
|
|
268
|
+
let attrs = node[':@'] || {};
|
|
269
|
+
for (let [key, value] of Object.entries(attrs)) {
|
|
270
|
+
if (
|
|
271
|
+
!(key === 'version' && /^1\.[0-9]+$/.test(value)) &&
|
|
272
|
+
!(key === 'encoding' && /^[A-Za-z][A-Za-z0-9._-]*$/.test(value)) &&
|
|
273
|
+
!(key === 'standalone' && /^(?:yes|no)$/.test(value))
|
|
274
|
+
) {
|
|
275
|
+
throw invalidXml('Invalid XML declaration');
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
} else if (name && name.charAt(0) === '?') {
|
|
279
|
+
// for example xml-stylesheet, which loads an external style sheet
|
|
280
|
+
throw createError('Processing instructions are not allowed', 'LOGO_INVALID_CONTENT', { details: { instruction: name.slice(1) } });
|
|
281
|
+
} else if (name && ++rootCount > 1) {
|
|
282
|
+
throw invalidXml('Multiple root elements');
|
|
283
|
+
}
|
|
46
284
|
}
|
|
47
285
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
286
|
+
// returns the namespace of a prefix in the current scope
|
|
287
|
+
const resolvePrefix = (scope, prefix, path) => {
|
|
288
|
+
if (prefix === 'xml') {
|
|
289
|
+
return XML_NS;
|
|
51
290
|
}
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
let error = new Error('External reference found from file');
|
|
59
|
-
error.details = {
|
|
60
|
-
element: name,
|
|
61
|
-
link: node['@_xlink:href'],
|
|
62
|
-
path
|
|
63
|
-
};
|
|
64
|
-
error.code = 'LOGO_INCLUDES_REFERENCE';
|
|
65
|
-
throw error;
|
|
291
|
+
if (prefix === 'xmlns') {
|
|
292
|
+
return XMLNS_NS;
|
|
293
|
+
}
|
|
294
|
+
if (!Object.prototype.hasOwnProperty.call(scope, prefix)) {
|
|
295
|
+
if (!prefix) {
|
|
296
|
+
return null;
|
|
66
297
|
}
|
|
298
|
+
throw invalidXml(`Undeclared namespace prefix "${prefix}"`, { path });
|
|
299
|
+
}
|
|
300
|
+
return scope[prefix];
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
let rootAttrs = null;
|
|
304
|
+
let titleCount = 0;
|
|
305
|
+
let titleText = '';
|
|
306
|
+
let firstChild = true;
|
|
307
|
+
|
|
308
|
+
// checks the content of a text node or a CDATA section
|
|
309
|
+
const checkText = (value, path) => {
|
|
310
|
+
// Text of a CDATA section is not escaped. An HTML parser does not handle CDATA in title or
|
|
311
|
+
// desc, and would read a tag from it.
|
|
312
|
+
if (value.indexOf('<') >= 0) {
|
|
313
|
+
throw createError('Unexpected markup in text content', 'LOGO_INVALID_CONTENT', { details: { path } });
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const walk = (node, scope, path, depth, inMetadata) => {
|
|
318
|
+
if (depth > MAX_DEPTH) {
|
|
319
|
+
throw createError('Document is nested too deeply', 'LOGO_INVALID_CONTENT', { details: { path } });
|
|
320
|
+
}
|
|
67
321
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
322
|
+
let qname = nodeName(node);
|
|
323
|
+
let attrs = node[':@'] || {};
|
|
324
|
+
|
|
325
|
+
// namespace declarations of this element
|
|
326
|
+
let localScope = scope;
|
|
327
|
+
for (let [key, value] of Object.entries(attrs)) {
|
|
328
|
+
if (key === 'xmlns' || key.indexOf('xmlns:') === 0) {
|
|
329
|
+
if (localScope === scope) {
|
|
330
|
+
localScope = Object.assign({}, scope);
|
|
77
331
|
}
|
|
332
|
+
let decoded = decodeEntities(value);
|
|
333
|
+
localScope[key === 'xmlns' ? '' : key.slice(6)] = decoded || null;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
let { prefix, local } = splitName(qname);
|
|
338
|
+
let namespace = resolvePrefix(localScope, prefix, path);
|
|
339
|
+
let elementPath = `${path}.${qname}`;
|
|
340
|
+
|
|
341
|
+
if (depth === 0 && namespace !== SVG_NS) {
|
|
342
|
+
// 2.1: xmlns="http://www.w3.org/2000/svg", in any other namespace this is not an SVG document
|
|
343
|
+
throw createError('Invalid SVG file', 'INVALID_SVG_FILE', { details: { namespace } });
|
|
344
|
+
}
|
|
345
|
+
let isSvg = namespace === SVG_NS;
|
|
346
|
+
|
|
347
|
+
// attributes are checked first, so an external reference is reported as such
|
|
348
|
+
for (let [key, rawValue] of Object.entries(attrs)) {
|
|
349
|
+
if (key === 'xmlns' || key.indexOf('xmlns:') === 0) {
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
let attr = splitName(key);
|
|
353
|
+
// an unprefixed attribute has no namespace
|
|
354
|
+
let attrNs = attr.prefix ? resolvePrefix(localScope, attr.prefix, elementPath) : null;
|
|
355
|
+
if (rawValue.indexOf('<') >= 0) {
|
|
356
|
+
throw invalidXml('Unescaped "<" in an attribute value', { path: elementPath, attribute: key });
|
|
357
|
+
}
|
|
358
|
+
let value = decodeEntities(rawValue);
|
|
78
359
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
360
|
+
if (/^on/i.test(attr.local)) {
|
|
361
|
+
throw createError('Event handler attribute found from file', 'LOGO_INVALID_ATTRIBUTE', {
|
|
362
|
+
details: {
|
|
363
|
+
element: qname,
|
|
364
|
+
attribute: key,
|
|
365
|
+
path: elementPath
|
|
82
366
|
}
|
|
83
|
-
}
|
|
84
|
-
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// href in the XLink namespace, without namespace (SVG 2), or with any prefix an HTML parser
|
|
371
|
+
// could map to XLink. Only same-document references are allowed (section 2.3, Linking).
|
|
372
|
+
if (attr.local.toLowerCase() === 'href' && value.trim().charAt(0) !== '#') {
|
|
373
|
+
throw externalReference({ element: qname, link: value, path: elementPath });
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (
|
|
377
|
+
isSvg &&
|
|
378
|
+
!attr.prefix &&
|
|
379
|
+
Object.prototype.hasOwnProperty.call(FIXED_ATTRIBUTE_VALUES, attr.local) &&
|
|
380
|
+
value.trim() !== FIXED_ATTRIBUTE_VALUES[attr.local]
|
|
381
|
+
) {
|
|
382
|
+
lax(
|
|
383
|
+
'svg-attribute-value',
|
|
384
|
+
createError(`${attr.local} must be "${FIXED_ATTRIBUTE_VALUES[attr.local]}"`, 'LOGO_INVALID_ATTRIBUTE', {
|
|
385
|
+
details: {
|
|
386
|
+
element: qname,
|
|
387
|
+
attribute: key,
|
|
388
|
+
value,
|
|
389
|
+
path: elementPath
|
|
390
|
+
}
|
|
391
|
+
})
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (attrNs === XML_NS && attr.local === 'base' && value.trim()) {
|
|
396
|
+
// a base URI could turn a fragment reference into an external one
|
|
397
|
+
throw createError('xml:base attribute found from file', 'LOGO_INVALID_ATTRIBUTE', {
|
|
398
|
+
details: { element: qname, attribute: key, path: elementPath }
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// presentation attributes and the style attribute are parsed as CSS
|
|
403
|
+
let cssError = checkCss(value);
|
|
404
|
+
if (cssError) {
|
|
405
|
+
throw externalReference({ element: qname, attribute: key, link: value, reason: cssError, path: elementPath });
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const invalidElement = reason =>
|
|
410
|
+
createError('Unallowed element found from file', 'LOGO_INVALID_ELEMENT', {
|
|
411
|
+
details: {
|
|
412
|
+
element: qname,
|
|
413
|
+
namespace,
|
|
414
|
+
reason,
|
|
415
|
+
path: elementPath
|
|
85
416
|
}
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
// An HTML parser ignores namespace declarations and treats an unprefixed element in SVG
|
|
420
|
+
// content as an SVG element, so an unprefixed name must be allowed in any namespace.
|
|
421
|
+
if ((isSvg || !prefix) && !TINY_PS_ELEMENTS.has(local) && !STATIC_ELEMENTS.has(local)) {
|
|
422
|
+
throw invalidElement('not allowed in SVG Tiny PS');
|
|
423
|
+
}
|
|
424
|
+
if (namespace && ACTIVE_NAMESPACES.has(namespace)) {
|
|
425
|
+
throw invalidElement('not allowed in SVG Tiny PS');
|
|
426
|
+
}
|
|
427
|
+
if (!prefix && local === 'font' && ['color', 'face', 'size'].some(key => key in attrs)) {
|
|
428
|
+
// an HTML parser would end the SVG content here and continue in HTML
|
|
429
|
+
throw invalidElement('font element with HTML attributes');
|
|
430
|
+
}
|
|
431
|
+
if (inMetadata && isSvg) {
|
|
432
|
+
throw invalidElement('SVG element in metadata');
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// the validation schema (section 7) does not allow these, they are not unsafe
|
|
436
|
+
if (inMetadata) {
|
|
437
|
+
// metadata only has text content in the schema
|
|
438
|
+
lax('svg-metadata-content', invalidElement('element in metadata'));
|
|
439
|
+
} else if (!isSvg) {
|
|
440
|
+
lax('svg-foreign-element', invalidElement('element in another namespace'));
|
|
441
|
+
} else if (STATIC_ELEMENTS.has(local)) {
|
|
442
|
+
lax('svg-element', invalidElement('not part of the SVG Tiny PS profile'));
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (depth === 0) {
|
|
446
|
+
rootAttrs = attrs;
|
|
447
|
+
} else if (isSvg && local === 'title' && depth === 1) {
|
|
448
|
+
// 2.1: one non-empty title, the schema has it as the first child of the root element
|
|
449
|
+
titleCount++;
|
|
450
|
+
if (titleCount === 1) {
|
|
451
|
+
titleText = (node[qname] || [])
|
|
452
|
+
.map(child => (child['#text'] !== undefined ? decodeEntities(child['#text']) : child['#cdata'] ? childText(child) : ''))
|
|
453
|
+
.join('');
|
|
454
|
+
if (!firstChild) {
|
|
455
|
+
lax(
|
|
456
|
+
'svg-title-position',
|
|
457
|
+
createError('The title element must be the first child of the svg element', 'LOGO_INVALID_ELEMENT', { details: { path: elementPath } })
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
} else {
|
|
461
|
+
lax('svg-title-count', createError('Logo file has more than one title', 'LOGO_MULTIPLE_TITLES', { details: { path: elementPath } }));
|
|
462
|
+
}
|
|
463
|
+
} else if (isSvg && local === 'title') {
|
|
464
|
+
lax(
|
|
465
|
+
'svg-title-position',
|
|
466
|
+
createError('The title element must be a child of the svg element', 'LOGO_INVALID_ELEMENT', { details: { path: elementPath } })
|
|
467
|
+
);
|
|
468
|
+
} else if (isSvg && local === 'desc' && !childText(node).trim()) {
|
|
469
|
+
// 2.1: if present, the contents of desc MUST NOT be empty
|
|
470
|
+
lax('svg-empty-desc', createError('The desc element is empty', 'LOGO_INVALID_CONTENT', { details: { path: elementPath } }));
|
|
471
|
+
}
|
|
472
|
+
if (depth === 1) {
|
|
473
|
+
firstChild = false;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
let isStyle = isSvg && local === 'style';
|
|
477
|
+
const checkStyle = text => {
|
|
478
|
+
let cssError = isStyle && checkCss(text);
|
|
479
|
+
if (cssError) {
|
|
480
|
+
throw externalReference({ element: qname, link: text, reason: cssError, path: elementPath });
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
let childMetadata = inMetadata || (isSvg && local === 'metadata');
|
|
484
|
+
|
|
485
|
+
for (let child of node[qname] || []) {
|
|
486
|
+
let childName = nodeName(child);
|
|
487
|
+
if (childName === '#text') {
|
|
488
|
+
checkText(child['#text'] || '', elementPath);
|
|
489
|
+
checkStyle(decodeEntities(child['#text'] || ''));
|
|
490
|
+
} else if (childName === '#cdata') {
|
|
491
|
+
let text = childText(child);
|
|
492
|
+
checkText(text, elementPath);
|
|
493
|
+
checkStyle(text);
|
|
494
|
+
} else if (childName === '#comment') {
|
|
495
|
+
checkComment(child, elementPath);
|
|
496
|
+
} else if (childName && childName.charAt(0) === '?') {
|
|
497
|
+
throw createError('Processing instructions are not allowed', 'LOGO_INVALID_CONTENT', {
|
|
498
|
+
details: { instruction: childName.slice(1), path: elementPath }
|
|
499
|
+
});
|
|
500
|
+
} else if (childName) {
|
|
501
|
+
walk(child, localScope, elementPath, depth + 1, childMetadata);
|
|
86
502
|
}
|
|
87
503
|
}
|
|
88
504
|
};
|
|
89
505
|
|
|
90
|
-
|
|
506
|
+
walk(root, {}, '', 0, false);
|
|
507
|
+
|
|
508
|
+
if (rootAttrs.baseProfile !== 'tiny-ps') {
|
|
509
|
+
throw createError('Not a Tiny PS profile', 'INVALID_BASE_PROFILE');
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (!titleCount || !titleText.trim()) {
|
|
513
|
+
throw createError('Logo file is missing title', 'LOGO_MISSING_TITLE');
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if ('x' in rootAttrs || 'y' in rootAttrs) {
|
|
517
|
+
throw createError('Logo root includes x/y attributes', 'LOGO_INVALID_ROOT_ATTRS');
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (rootAttrs.version !== '1.2') {
|
|
521
|
+
// 2.1: version="1.2"
|
|
522
|
+
lax('svg-version', createError('SVG version must be 1.2', 'INVALID_SVG_VERSION', { details: { version: rootAttrs.version } }));
|
|
523
|
+
}
|
|
91
524
|
|
|
92
525
|
// all validations passed
|
|
93
526
|
return true;
|
package/lib/commands/report.js
CHANGED
|
@@ -34,11 +34,11 @@ const cmd = async argv => {
|
|
|
34
34
|
opts.ip = argv.clientIp;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
if (argv.maxLookups) {
|
|
37
|
+
if (typeof argv.maxLookups === 'number') {
|
|
38
38
|
opts.maxResolveCount = argv.maxLookups;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
if (argv.maxVoidLookups) {
|
|
41
|
+
if (typeof argv.maxVoidLookups === 'number') {
|
|
42
42
|
opts.maxVoidCount = argv.maxVoidLookups;
|
|
43
43
|
}
|
|
44
44
|
|
package/lib/commands/seal.js
CHANGED
|
@@ -75,6 +75,9 @@ const cmd = async argv => {
|
|
|
75
75
|
if (argv.authResults || argv.authResultsFile) {
|
|
76
76
|
// Seal-only mode: embed the caller-provided Authentication-Results value without authenticating the message
|
|
77
77
|
let authResults = argv.authResultsFile ? (await fs.promises.readFile(argv.authResultsFile, 'utf-8')).trim() : argv.authResults;
|
|
78
|
+
// a folded value from a file usually has LF line endings, header folding needs CRLF.
|
|
79
|
+
// createSeal refuses any line break that does not continue the header with whitespace
|
|
80
|
+
authResults = authResults.replace(/\r?\n/g, '\r\n');
|
|
78
81
|
|
|
79
82
|
let sealOpts = Object.assign({}, signatureOpts, {
|
|
80
83
|
authResults,
|
|
@@ -95,7 +98,7 @@ const cmd = async argv => {
|
|
|
95
98
|
console.error('--------');
|
|
96
99
|
}
|
|
97
100
|
|
|
98
|
-
let { headers, errors, warnings } = await createSeal(gatherer, { seal: sealOpts });
|
|
101
|
+
let { headers, errors, warnings, instance } = await createSeal(gatherer, { seal: sealOpts });
|
|
99
102
|
|
|
100
103
|
if (!headers.length) {
|
|
101
104
|
// nothing was sealed, say why instead of printing the message as if it had been
|
|
@@ -103,20 +106,19 @@ const cmd = async argv => {
|
|
|
103
106
|
throw err;
|
|
104
107
|
}
|
|
105
108
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
console.error(`warning: sealed with i=${sealOpts.i} and cv=none; a chain with i>1 only validates when cv=pass (use --cv pass)`);
|
|
109
|
+
if (instance > 1 && !argv.cv) {
|
|
110
|
+
console.error(`warning: sealed with i=${instance} and cv=none; a chain with i>1 only validates when cv=pass (use --cv pass)`);
|
|
109
111
|
}
|
|
110
112
|
|
|
111
113
|
for (let warning of warnings || []) {
|
|
112
114
|
switch (warning) {
|
|
113
115
|
case 'arc-instance-gap':
|
|
114
|
-
console.error(`warning: sealed with i=${
|
|
116
|
+
console.error(`warning: sealed with i=${instance}, which does not follow the existing ARC chain`);
|
|
115
117
|
break;
|
|
116
118
|
case 'arc-cv-instance':
|
|
117
119
|
// covered by the message above for the default cv
|
|
118
120
|
if (argv.cv) {
|
|
119
|
-
console.error(`warning: cv=${argv.cv} is not valid for i=${
|
|
121
|
+
console.error(`warning: cv=${argv.cv} is not valid for i=${instance} (i=1 takes cv=none, later sets take cv=pass or cv=fail)`);
|
|
120
122
|
}
|
|
121
123
|
break;
|
|
122
124
|
default:
|
package/lib/commands/spf.js
CHANGED
|
@@ -24,11 +24,11 @@ const cmd = async argv => {
|
|
|
24
24
|
opts.ip = argv.clientIp;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
if (argv.maxLookups) {
|
|
27
|
+
if (typeof argv.maxLookups === 'number') {
|
|
28
28
|
opts.maxResolveCount = argv.maxLookups;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
if (argv.maxVoidLookups) {
|
|
31
|
+
if (typeof argv.maxVoidLookups === 'number') {
|
|
32
32
|
opts.maxVoidCount = argv.maxVoidLookups;
|
|
33
33
|
}
|
|
34
34
|
|