dmarcator 1.2.1 → 1.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c588b9e28df99c70cd55c29c83854064092e2ff74436c9be680afc2b2a77824c
4
- data.tar.gz: 3e6692aa15ea2aa28fa451dc847c3cadbd0cb36cdaf237e8bcb6e591206f92d0
3
+ metadata.gz: cbafae55c92fefeee56b1e5d5e714433cd595facbeb078925b5f54ae55c05ffb
4
+ data.tar.gz: 9be9ee7752f5ec16a6c7c6e2da499e909a9ff4d5d05c11af8057bf760bff4bac
5
5
  SHA512:
6
- metadata.gz: 890fe93593dd8ae6cc2888910070d73606f2700ce3e6548b09cbfcca29f86067788894dbdd23d984a23a7662059514564f955ffdf476656d94110289603b8d4c
7
- data.tar.gz: b3357457dbeb21bcd40dee7a412eacc9df9d60b148eae9b0f753c2ca6534d299f4ff6bd7585fcfcf67d4482eabc4f6227ac0878af1d839721de1e2cd7334853f
6
+ metadata.gz: 4206b0cd61595bcffb31487edf88fc6bb7bbc36a3148ab94dec83c5911b49a9d5b05de05dac01c13c232b733ed3a3d40bcd3ee1c96ed123a0a6dbbe812fded3e
7
+ data.tar.gz: dafdc7d98f728a42398a5a8e9c9c05709592b6e5b341d6b7e5848176b6a3563388608432f30f4e5a538621ba163107880357bdf7ff2e85aee6d1ba7623e576a1
data/ChangeLog.md CHANGED
@@ -1,3 +1,18 @@
1
+ ### 1.4.0 / 2026-11-01
2
+
3
+ * Enforce the per-record `max_auth_results` and `max_reasons` limits eagerly during the streaming pre-check, before any DOM is built. Previously a report with one record containing an enormous `auth_results`/`reason` collection reached DOM construction unless another global limit caught it.
4
+ * Require a `<feedback>` root element: well-formed XML that is not a DMARC aggregate report is rejected with `InvalidReport` instead of being parsed into an empty report.
5
+ * Stop echoing libxml2 error text (which can contain tag, attribute or entity names from the untrusted input) in `InvalidReport` messages; report only line and column position.
6
+ * Add near-limit regression tests (reports just below every configured limit still parse) and a deterministic fuzz test that random bytes, tag soup, huge text and many attributes only ever raise `DMARCator::Error`.
7
+
8
+ ### 1.3.0 / 2026-11-01
9
+
10
+ * Treat report XML as hostile by rejecting DTD and entity declarations outright (`DMARCator::ForbiddenXml`). DMARC reports never need a DTD, so external-entity (XXE) and entity-expansion (billion-laughs) attacks are rejected before parsing rather than merely having their resolution disabled.
11
+ * Parse report XML with an explicit hardening policy: `NONET` on, entity substitution, DTD loading, default DTD attributes and libxml2's `HUGE` relaxation all off, and strict (non-recovering) parsing.
12
+ * Bound report structural complexity with configurable limits enforced by a streaming reader before any DOM is built. Exceeding a limit raises `DMARCator::ReportTooComplex` rather than `InvalidReport`.
13
+ * Add `Config` limits `max_report_records` (10,000), `max_xml_nodes` (100,000), `max_xml_depth` (50), `max_text_length` (1 MiB), `max_xml_attributes` (100,000), `max_auth_results` (100) and `max_reasons` (100).
14
+ * Add `DMARCator::ReportTooComplex` and `DMARCator::ForbiddenXml` error classes (both `DMARCator::Error`).
15
+
1
16
  ### 1.2.1 / 2026-10-30
2
17
 
3
18
  * Add a record-parser corpus regression net: 143 records (published real-world records plus edge cases) with their accepted outcomes in lenient and strict mode, replayed by the test suite. A parser change that alters how any record parses now fails with a diff. Regenerate with `script/generate_record_corpus.rb`.
data/README.md CHANGED
@@ -202,6 +202,15 @@ Process-wide defaults can be set from code, and take precedence over the
202
202
  DMARCator.configure do |config|
203
203
  config.resolver = DMARCator::Resolver::DoH.new(url: "https://dns.example/dns-query")
204
204
  config.max_report_bytes = 5 * 1024 * 1024
205
+
206
+ # XML report complexity limits (defaults shown)
207
+ config.max_report_records = 10_000
208
+ config.max_xml_nodes = 100_000
209
+ config.max_xml_depth = 50
210
+ config.max_text_length = 1 * 1024 * 1024
211
+ config.max_xml_attributes = 100_000
212
+ config.max_auth_results = 100
213
+ config.max_reasons = 100
205
214
  end
206
215
  ```
207
216
 
@@ -334,15 +343,39 @@ DMARC records and XML reports are treated as untrusted input. See
334
343
  [SECURITY.md](https://github.com/bermannoah/dmarcator/blob/main/SECURITY.md)
335
344
  for the reporting policy and scope.
336
345
 
337
- * XML reports are parsed with network access disabled (`NONET`), so external
338
- entities are never resolved or fetched.
339
- * Reports larger than `DMARCator.config.max_report_bytes` (25 MiB by
340
- default) are rejected before parsing. Parsing builds a DOM roughly 13× the
341
- input size, so lower the limit to bound memory:
346
+ * XML reports are treated as hostile: DTDs and entity declarations are
347
+ rejected outright (`DMARCator::ForbiddenXml`), and parsing disables network
348
+ access, entity substitution, DTD loading, and libxml2's `HUGE` relaxation.
349
+ External-entity (XXE) and entity-expansion (billion-laughs) attacks are
350
+ therefore impossible rather than merely mitigated. A report must have a
351
+ `<feedback>` root element; arbitrary well-formed XML is rejected with
352
+ `DMARCator::InvalidReport`.
353
+ * Report structural complexity is bounded before any DOM is built. Every limit
354
+ below — including the per-record `auth_results`/`reason` limits — is enforced
355
+ by a streaming pre-check, so a report that exceeds a configured limit raises
356
+ `DMARCator::ReportTooComplex` before a DOM is allocated:
357
+
358
+ | Setting | Default | Bounds |
359
+ |---|---|---|
360
+ | `max_report_bytes` | 25 MiB | input size |
361
+ | `max_report_records` | 10,000 | `record` elements |
362
+ | `max_xml_nodes` | 100,000 | element/text/comment nodes |
363
+ | `max_xml_depth` | 50 | element nesting depth |
364
+ | `max_text_length` | 1 MiB | bytes per text node |
365
+ | `max_xml_attributes` | 100,000 | attribute nodes |
366
+ | `max_auth_results` | 100 | `auth_results` entries per record |
367
+ | `max_reasons` | 100 | `reason` entries per record |
368
+
369
+ Lower them to bound memory and CPU for reports from untrusted sources:
342
370
 
343
371
  ```ruby
344
372
  DMARCator.configure { |config| config.max_report_bytes = 5 * 1024 * 1024 }
345
373
  ```
374
+
375
+ These limits are not the outer boundary. They bound the work the C parser is
376
+ asked to do, but a libxml2 bug can still bypass Ruby-level checks. Applications
377
+ parsing reports from untrusted sources should still run the parse in an
378
+ isolated worker or subprocess with its own resource limits.
346
379
  * A warning is emitted at load time if the bundled `resolv` gem is affected by
347
380
  CVE-2026-80212/CVE-2026-80213.
348
381
  * Domain names are validated before resolution (labels ≤ 63 octets, total ≤
@@ -16,6 +16,54 @@ module DMARCator
16
16
  #
17
17
  DEFAULT_MAX_REPORT_BYTES = 25 * 1024 * 1024
18
18
 
19
+ # Default maximum number of `record` elements accepted in a report.
20
+ #
21
+ # @since 1.3.0
22
+ #
23
+ DEFAULT_MAX_REPORT_RECORDS = 10_000
24
+
25
+ # Default maximum number of XML nodes accepted in a report.
26
+ #
27
+ # Counts element, text, CDATA, whitespace, comment and processing
28
+ # instruction nodes; attributes are counted separately by
29
+ # {DEFAULT_MAX_XML_ATTRIBUTES}.
30
+ #
31
+ # @since 1.3.0
32
+ #
33
+ DEFAULT_MAX_XML_NODES = 100_000
34
+
35
+ # Default maximum element nesting depth accepted in a report.
36
+ #
37
+ # The root element is depth 1.
38
+ #
39
+ # @since 1.3.0
40
+ #
41
+ DEFAULT_MAX_XML_DEPTH = 50
42
+
43
+ # Default maximum length in bytes of any single text node.
44
+ #
45
+ # @since 1.3.0
46
+ #
47
+ DEFAULT_MAX_TEXT_LENGTH = 1 * 1024 * 1024
48
+
49
+ # Default maximum number of attribute nodes accepted in a report.
50
+ #
51
+ # @since 1.3.0
52
+ #
53
+ DEFAULT_MAX_XML_ATTRIBUTES = 100_000
54
+
55
+ # Default maximum number of `auth_results` entries per record.
56
+ #
57
+ # @since 1.3.0
58
+ #
59
+ DEFAULT_MAX_AUTH_RESULTS = 100
60
+
61
+ # Default maximum number of `reason` entries per record.
62
+ #
63
+ # @since 1.3.0
64
+ #
65
+ DEFAULT_MAX_REASONS = 100
66
+
19
67
  #
20
68
  # The default resolver used when a lookup is not given one.
21
69
  #
@@ -57,6 +105,78 @@ module DMARCator
57
105
 
58
106
  @max_report_bytes = value
59
107
  end
108
+
109
+ #
110
+ # XML report complexity limits.
111
+ #
112
+ # Each accepts a positive Integer or `nil` (to restore the default). A
113
+ # report that exceeds one of these raises {ReportTooComplex} when parsed.
114
+ # They bound parser work on hostile input; see {DMARCator::Report}.
115
+ #
116
+ # @!method max_report_records
117
+ # @return [Integer] the maximum number of `record` elements.
118
+ # @!method max_report_records=(value)
119
+ # @param [Integer, nil] value
120
+ # @raise [ArgumentError] the value is not a positive Integer or nil.
121
+ #
122
+ # @!method max_xml_nodes
123
+ # @return [Integer] the maximum number of XML nodes.
124
+ # @!method max_xml_nodes=(value)
125
+ # @param [Integer, nil] value
126
+ # @raise [ArgumentError] the value is not a positive Integer or nil.
127
+ #
128
+ # @!method max_xml_depth
129
+ # @return [Integer] the maximum element nesting depth (root is 1).
130
+ # @!method max_xml_depth=(value)
131
+ # @param [Integer, nil] value
132
+ # @raise [ArgumentError] the value is not a positive Integer or nil.
133
+ #
134
+ # @!method max_text_length
135
+ # @return [Integer] the maximum text node length in bytes.
136
+ # @!method max_text_length=(value)
137
+ # @param [Integer, nil] value
138
+ # @raise [ArgumentError] the value is not a positive Integer or nil.
139
+ #
140
+ # @!method max_xml_attributes
141
+ # @return [Integer] the maximum number of attribute nodes.
142
+ # @!method max_xml_attributes=(value)
143
+ # @param [Integer, nil] value
144
+ # @raise [ArgumentError] the value is not a positive Integer or nil.
145
+ #
146
+ # @!method max_auth_results
147
+ # @return [Integer] the maximum number of `auth_results` entries per record.
148
+ # @!method max_auth_results=(value)
149
+ # @param [Integer, nil] value
150
+ # @raise [ArgumentError] the value is not a positive Integer or nil.
151
+ #
152
+ # @!method max_reasons
153
+ # @return [Integer] the maximum number of `reason` entries per record.
154
+ # @!method max_reasons=(value)
155
+ # @param [Integer, nil] value
156
+ # @raise [ArgumentError] the value is not a positive Integer or nil.
157
+ #
158
+ {
159
+ max_report_records: DEFAULT_MAX_REPORT_RECORDS,
160
+ max_xml_nodes: DEFAULT_MAX_XML_NODES,
161
+ max_xml_depth: DEFAULT_MAX_XML_DEPTH,
162
+ max_text_length: DEFAULT_MAX_TEXT_LENGTH,
163
+ max_xml_attributes: DEFAULT_MAX_XML_ATTRIBUTES,
164
+ max_auth_results: DEFAULT_MAX_AUTH_RESULTS,
165
+ max_reasons: DEFAULT_MAX_REASONS
166
+ }.each do |name, default|
167
+ define_method(name) do
168
+ instance_variable_get(:"@#{name}") || default
169
+ end
170
+
171
+ define_method(:"#{name}=") do |value|
172
+ unless value.nil? || (value.is_a?(Integer) && value.positive?)
173
+ raise ArgumentError,
174
+ "#{name} must be a positive Integer or nil, got #{value.inspect}"
175
+ end
176
+
177
+ instance_variable_set(:"@#{name}", value)
178
+ end
179
+ end
60
180
  end
61
181
 
62
182
  #
@@ -28,6 +28,32 @@ module DMARCator
28
28
  class InvalidReport < Error
29
29
  end
30
30
 
31
+ #
32
+ # Raised when a DMARC XML report is well-formed but exceeds a configured
33
+ # structural limit (record count, node count, nesting depth, text length,
34
+ # attribute count, or per-record auth_results/reasons entries).
35
+ #
36
+ # Distinct from {InvalidReport} so an application can audit or reject
37
+ # complexity attacks without treating them as ordinary malformed input.
38
+ #
39
+ # @since 1.3.0
40
+ #
41
+ class ReportTooComplex < Error
42
+ end
43
+
44
+ #
45
+ # Raised when a DMARC XML report contains a DTD or entity declaration.
46
+ #
47
+ # DMARC reports never need a DTD or custom entities, so declaring them is
48
+ # rejected outright. This eliminates external-entity (XXE) and
49
+ # entity-expansion (billion-laughs) attacks rather than merely disabling
50
+ # their resolution.
51
+ #
52
+ # @since 1.3.0
53
+ #
54
+ class ForbiddenXml < Error
55
+ end
56
+
31
57
  #
32
58
  # Raised when a DNS transport fails to return a usable answer.
33
59
  #
@@ -0,0 +1,232 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'nokogiri'
4
+
5
+ require_relative '../config'
6
+ require_relative '../exceptions'
7
+
8
+ module DMARCator
9
+ #
10
+ # Implementation details of the library. Nothing here is covered by the
11
+ # 1.0 compatibility promise.
12
+ #
13
+ # @api private
14
+ #
15
+ module Internal
16
+ #
17
+ # Hardens DMARC XML report parsing against hostile input.
18
+ #
19
+ # A DMARC report never needs a DTD or custom entities, so declarations are
20
+ # rejected outright, and libxml2 is configured to disable everything that
21
+ # could fetch or expand them. Structural complexity is bounded with a
22
+ # streaming reader pass before any DOM is built, so a report that exceeds a
23
+ # limit is rejected without first allocating a tree for it. Every configured
24
+ # limit — including the per-record `auth_results` and `reason` limits — is
25
+ # enforced here, before DOM allocation.
26
+ #
27
+ # A report must have a `<feedback>` root element; arbitrary well-formed XML
28
+ # that is not a DMARC aggregate report is rejected.
29
+ #
30
+ # Ruby's `Timeout` is deliberately not used here: it cannot reliably
31
+ # interrupt libxml2's C-level parsing. Callers that need a hard wall-clock
32
+ # bound must run the parse in a subprocess with resource limits; the limits
33
+ # enforced here bound the work the C parser is asked to do.
34
+ #
35
+ # @api private
36
+ #
37
+ class XmlGuard
38
+ # DTD and entity declarations, which are never legitimate in a report.
39
+ #
40
+ # Matched case-insensitively against the raw bytes as a cheap first line
41
+ # of defence; the streaming pass also rejects the parsed
42
+ # `DOCTYPE`/`ENTITY` nodes, which covers encodings this byte scan cannot
43
+ # see (for example UTF-16).
44
+ FORBIDDEN_DECLARATION = /<!doctype|<!entity/i
45
+
46
+ # Message raised when a DTD or entity declaration is present.
47
+ #
48
+ # Kept free of input-derived content: declaration details are never
49
+ # echoed to callers.
50
+ FORBIDDEN_MESSAGE = 'DMARC XML reports must not contain a DTD or entity declaration'
51
+
52
+ #
53
+ # Parse options shared by the streaming reader and the DOM parser.
54
+ #
55
+ # * `nonet` forbids network access (external entities are never fetched).
56
+ # * `nonoent`, `nodtdload` and `nodtdattr` keep entity substitution and
57
+ # DTD loading disabled.
58
+ # * `nohuge` keeps libxml2's own hardcoded limits in force.
59
+ # * The base options are strict (`ParseOptions.new`), so malformed input
60
+ # is reported rather than silently recovered.
61
+ #
62
+ # @return [Nokogiri::XML::ParseOptions]
63
+ #
64
+ def self.parse_options
65
+ Nokogiri::XML::ParseOptions.new.tap do |options|
66
+ options.nonet
67
+ options.nonoent
68
+ options.nodtdload
69
+ options.nodtdattr
70
+ options.nohuge
71
+ end
72
+ end
73
+
74
+ #
75
+ # @param [String] xml
76
+ # The raw DMARC XML report.
77
+ # @param [Config] config
78
+ # The configuration whose limits are enforced.
79
+ #
80
+ def initialize(xml, config = DMARCator.config)
81
+ @xml = xml
82
+ @config = config
83
+ end
84
+
85
+ #
86
+ # Rejects forbidden declarations and enforces structural limits without
87
+ # building a DOM.
88
+ #
89
+ # @return [XmlGuard] self
90
+ #
91
+ # @raise [ForbiddenXml]
92
+ # The report contains a DTD or entity declaration.
93
+ # @raise [ReportTooComplex]
94
+ # The report exceeds a configured structural limit.
95
+ # @raise [InvalidReport]
96
+ # The report is not well-formed XML or its encoding is malformed.
97
+ #
98
+ def check!
99
+ reject_forbidden_declarations!
100
+ enforce_structural_limits!
101
+ self
102
+ end
103
+
104
+ #
105
+ # Parses the report into a hardened DOM.
106
+ #
107
+ # @return [Nokogiri::XML::Document]
108
+ #
109
+ # @raise [InvalidReport]
110
+ # The report is not well-formed XML or its encoding is malformed.
111
+ #
112
+ def document
113
+ @document ||= parse_document
114
+ end
115
+
116
+ private
117
+
118
+ def reject_forbidden_declarations!
119
+ haystack = if @xml.encoding.ascii_compatible? && @xml.valid_encoding?
120
+ @xml
121
+ else
122
+ @xml.b
123
+ end
124
+
125
+ return unless haystack.match?(FORBIDDEN_DECLARATION)
126
+
127
+ raise ForbiddenXml, FORBIDDEN_MESSAGE
128
+ end
129
+
130
+ def enforce_structural_limits!
131
+ reader = Nokogiri::XML::Reader(@xml, nil, nil, self.class.parse_options)
132
+
133
+ nodes = 0
134
+ records = 0
135
+ attributes = 0
136
+
137
+ stack = []
138
+ root_seen = false
139
+ record_auth_results = 0
140
+ record_reasons = 0
141
+
142
+ while reader.read
143
+ case reader.node_type
144
+ when Nokogiri::XML::Reader::TYPE_DOCUMENT_TYPE,
145
+ Nokogiri::XML::Reader::TYPE_ENTITY
146
+ raise ForbiddenXml, FORBIDDEN_MESSAGE
147
+ when Nokogiri::XML::Reader::TYPE_ELEMENT
148
+ nodes += 1
149
+ attributes += reader.attribute_count
150
+
151
+ enforce(:max_xml_nodes, nodes)
152
+ enforce(:max_xml_attributes, attributes)
153
+ enforce(:max_xml_depth, reader.depth + 1)
154
+
155
+ unless root_seen
156
+ root_seen = true
157
+
158
+ unless reader.name == 'feedback'
159
+ raise InvalidReport, 'DMARC XML report must have a <feedback> root element'
160
+ end
161
+ end
162
+
163
+ if reader.name == 'record'
164
+ records += 1
165
+ enforce(:max_report_records, records)
166
+
167
+ record_auth_results = 0
168
+ record_reasons = 0
169
+ elsif stack.include?('record')
170
+ if stack.last == 'auth_results'
171
+ record_auth_results += 1
172
+ enforce(:max_auth_results, record_auth_results)
173
+ elsif stack.last == 'policy_evaluated' && reader.name == 'reason'
174
+ record_reasons += 1
175
+ enforce(:max_reasons, record_reasons)
176
+ end
177
+ end
178
+
179
+ stack.push(reader.name) unless reader.empty_element?
180
+ when Nokogiri::XML::Reader::TYPE_TEXT,
181
+ Nokogiri::XML::Reader::TYPE_CDATA,
182
+ Nokogiri::XML::Reader::TYPE_WHITESPACE,
183
+ Nokogiri::XML::Reader::TYPE_SIGNIFICANT_WHITESPACE,
184
+ Nokogiri::XML::Reader::TYPE_COMMENT,
185
+ Nokogiri::XML::Reader::TYPE_PROCESSING_INSTRUCTION,
186
+ Nokogiri::XML::Reader::TYPE_ENTITY_REFERENCE
187
+ nodes += 1
188
+ enforce(:max_xml_nodes, nodes)
189
+
190
+ value = reader.value
191
+ enforce(:max_text_length, value.bytesize) if value
192
+ when Nokogiri::XML::Reader::TYPE_END_ELEMENT
193
+ stack.pop
194
+ end
195
+ end
196
+ rescue Nokogiri::XML::SyntaxError => e
197
+ raise InvalidReport, sanitize_syntax_error(e)
198
+ end
199
+
200
+ def enforce(setting, value)
201
+ limit = @config.public_send(setting)
202
+ return unless value > limit
203
+
204
+ raise ReportTooComplex, "DMARC XML report exceeds #{setting} of #{limit}"
205
+ end
206
+
207
+ #
208
+ # Renders a parse error using only position information. Nokogiri's
209
+ # messages can echo tag, attribute or entity names from the input, and
210
+ # those are untrusted; line/column numbers are not.
211
+ #
212
+ def sanitize_syntax_error(error)
213
+ line = error.respond_to?(:line) ? error.line : nil
214
+ column = error.respond_to?(:column) ? error.column : nil
215
+
216
+ if line && column
217
+ "invalid DMARC XML report at line #{line}, column #{column}"
218
+ else
219
+ 'invalid DMARC XML report'
220
+ end
221
+ end
222
+
223
+ def parse_document
224
+ document = Nokogiri::XML.parse(@xml, nil, nil, self.class.parse_options)
225
+
226
+ document
227
+ rescue Nokogiri::XML::SyntaxError => e
228
+ raise InvalidReport, sanitize_syntax_error(e)
229
+ end
230
+ end
231
+ end
232
+ end
@@ -1,8 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'nokogiri'
4
-
5
3
  require_relative 'config'
4
+ require_relative 'internal/xml_guard'
6
5
  require_relative 'internal/xml_helper'
7
6
  require_relative 'exceptions'
8
7
  require_relative 'report/metadata'
@@ -13,8 +12,10 @@ module DMARCator
13
12
  #
14
13
  # Represents a parsed DMARC aggregate/forensic XML report.
15
14
  #
16
- # Parsing disables network access (NONET) so external entities are never
17
- # resolved or fetched; treat report XML as untrusted input.
15
+ # Report XML is treated as untrusted input: DTDs and entity declarations are
16
+ # rejected, network access and entity expansion are disabled, and structural
17
+ # complexity (nodes, depth, records, text and attribute counts) is bounded by
18
+ # {DMARCator.configure} limits before any DOM is built.
18
19
  #
19
20
  class Report
20
21
  include Internal::XmlHelper
@@ -63,6 +64,10 @@ module DMARCator
63
64
  # @raise [InvalidReport]
64
65
  # The report is not a String, exceeds
65
66
  # {Config#max_report_bytes}, or is not well-formed XML.
67
+ # @raise [ForbiddenXml]
68
+ # The report contains a DTD or entity declaration.
69
+ # @raise [ReportTooComplex]
70
+ # The report exceeds a configured structural limit.
66
71
  #
67
72
  def initialize(xml)
68
73
  unless xml.is_a?(String)
@@ -77,12 +82,10 @@ module DMARCator
77
82
  raise InvalidReport, "DMARC XML report exceeds maximum size of #{limit} bytes"
78
83
  end
79
84
 
80
- @node = Nokogiri::XML(xml) { |config| config.nonet }
81
-
82
- unless @node.errors.empty?
83
- raise InvalidReport, "invalid DMARC XML report: #{@node.errors.first}"
84
- end
85
+ guard = Internal::XmlGuard.new(xml)
86
+ guard.check!
85
87
 
88
+ @node = guard.document
86
89
  @node.remove_namespaces!
87
90
  end
88
91
 
@@ -2,5 +2,5 @@
2
2
 
3
3
  module DMARCator
4
4
  # dmarcator version
5
- VERSION = '1.2.1'
5
+ VERSION = '1.4.0'
6
6
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dmarcator
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.1
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Noah Berman
@@ -38,6 +38,7 @@ files:
38
38
  - lib/dmarcator/config.rb
39
39
  - lib/dmarcator/exceptions.rb
40
40
  - lib/dmarcator/internal/parser.rb
41
+ - lib/dmarcator/internal/xml_guard.rb
41
42
  - lib/dmarcator/internal/xml_helper.rb
42
43
  - lib/dmarcator/query.rb
43
44
  - lib/dmarcator/record.rb