dmarcator 0.2.0 → 0.3.1

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: bdd3aafe769b2b0da2829b7247b4c65cd5cb15a28f41f1703e4b198938f43a58
4
- data.tar.gz: 22cd8f8dfed8f93c89b329af83b8b057288b15087033f2ee3646f17dbc631a06
3
+ metadata.gz: 00c852a4815b31a240c4c24071c523ff0af57f6d87b447fbcec7515f8b502e55
4
+ data.tar.gz: b325e8eb13e3ee9f91215800492a9d81142338ad71992c998f1d0976756b4214
5
5
  SHA512:
6
- metadata.gz: 18710be1b05e3d8c6301654a0ed9a9957d8d67eafa2e873e83cc3d43b0d1d4d2f2551a808b180177d3140cabc2bf3460be61578ae0ecba6d9b9e2ac4a0ad4a29
7
- data.tar.gz: e03b01fa23c6981ca858da830e73f08ddc6ca522dc28849d7efc076dbb0f6d319e41e8687522c5ecbe10bfeeda1e8726e9289ed8566711e9abc672e294ffdc54
6
+ metadata.gz: d7b2dd3cbcbd800cfc3b1de240200ec0493fa6d82794282f00aa4c23d209f4269f8335f55468091e4f69dad70206b56c6dcac33c5da578f385507a1bd3eb531a
7
+ data.tar.gz: 3faa47a127473ce206dc30d983772f5189d4d7cec302394ab96dcf64afe481463bfdaad645bf99e2c38cd14bd133d1c7c637744646dfed2d62662a5b105578f7
data/ChangeLog.md CHANGED
@@ -1,3 +1,17 @@
1
+ ### 0.3.1 / 2026-10-26
2
+
3
+ * Update the gemspec maintainer metadata to list only the fork's author.
4
+
5
+ ### 0.3.0 / 2026-10-26
6
+
7
+ * Raise `DMARCator::InvalidReport` for malformed XML reports instead of returning an empty report.
8
+ * Return `nil` for a non-numeric report `version` instead of raising `ArgumentError`.
9
+ * Return `nil` for a non-numeric report timestamp instead of coercing it to the Unix epoch.
10
+ * Add `DMARCator::Uri#eql?` and `#hash` for correct Hash/Set semantics.
11
+ * Harden `DMARCator.validate_domain` against CVE-2026-80213 (63-octet label limit and 248-octet total limit).
12
+ * Warn at load time when the `resolv` gem is affected by CVE-2026-80212/CVE-2026-80213.
13
+ * Require Ruby >= 3.3.13 to pick up the Regexp compilation fixes behind GitLab's CVE-2026-89078, CVE-2026-93577, CVE-2026-97229 and CVE-2026-110264.
14
+
1
15
  ### 0.2.0 / 2026-10-13
2
16
 
3
17
  * Added DMARC XML report parsing, ported from the `dmarc_parser` gem by D Salko.
data/README.md CHANGED
@@ -115,7 +115,7 @@ report.records.first.source_ip
115
115
 
116
116
  ## Requirements
117
117
 
118
- * [Ruby] >= 3.0.0
118
+ * [Ruby] >= 3.3.13
119
119
  * [parslet] >= 1.0.0, < 3.0.0
120
120
  * [nokogiri] >= 1.10
121
121
  * [bigdecimal] >= 1.0
@@ -3,4 +3,10 @@ require 'parslet'
3
3
  module DMARCator
4
4
  class InvalidRecord < Parslet::ParseFailed
5
5
  end
6
+
7
+ #
8
+ # Raised when a DMARC XML report cannot be parsed.
9
+ #
10
+ class InvalidReport < StandardError
11
+ end
6
12
  end
@@ -1,6 +1,43 @@
1
1
  require 'resolv'
2
+ require 'rubygems'
2
3
 
3
4
  module DMARCator
5
+ #
6
+ # Resolv gem versions affected by CVE-2026-80212 and CVE-2026-80213.
7
+ #
8
+ VULNERABLE_RESOLV = [
9
+ Gem::Requirement.new('< 0.3.2'),
10
+ Gem::Requirement.new('>= 0.4.0', '< 0.7.2')
11
+ ].freeze
12
+
13
+ #
14
+ # Determines the version of the loaded `resolv` gem, if available.
15
+ #
16
+ # @return [String, nil]
17
+ #
18
+ def self.resolv_version
19
+ if defined?(Resolv::VERSION)
20
+ Resolv::VERSION
21
+ elsif (spec = Gem.loaded_specs['resolv'])
22
+ spec.version.to_s
23
+ end
24
+ end
25
+
26
+ #
27
+ # Determines whether the loaded `resolv` gem is affected by
28
+ # CVE-2026-80212 or CVE-2026-80213.
29
+ #
30
+ # @return [Boolean]
31
+ #
32
+ def self.vulnerable_resolv?
33
+ version = resolv_version
34
+ return false unless version
35
+
36
+ VULNERABLE_RESOLV.any? do |requirement|
37
+ requirement.satisfied_by?(Gem::Version.new(version))
38
+ end
39
+ end
40
+
4
41
  #
5
42
  # Validates a domain before using it in a DNS query.
6
43
  #
@@ -15,10 +52,11 @@ module DMARCator
15
52
  #
16
53
  def self.validate_domain(domain)
17
54
  unless domain.is_a?(String) && !domain.empty? &&
18
- domain.length <= 253 &&
55
+ domain.length <= 248 &&
19
56
  !domain.start_with?('.') &&
20
57
  !domain.include?('..') &&
21
- !domain.match?(/[^A-Za-z0-9._-]/)
58
+ !domain.match?(/[^A-Za-z0-9._-]/) &&
59
+ domain.split('.').all? { |label| label.length <= 63 }
22
60
  raise ArgumentError, "invalid domain: #{domain.inspect}"
23
61
  end
24
62
 
@@ -61,3 +99,9 @@ module DMARCator
61
99
  nil
62
100
  end
63
101
  end
102
+
103
+ if DMARCator.vulnerable_resolv?
104
+ warn "dmarcator: resolv #{DMARCator.resolv_version} is affected by " \
105
+ "CVE-2026-80212 and CVE-2026-80213; upgrade to resolv >= 0.7.2 " \
106
+ "(or >= 0.3.2 on Ruby 3.3)"
107
+ end
@@ -5,6 +5,7 @@ require 'bigdecimal'
5
5
  require 'bigdecimal/util'
6
6
 
7
7
  require_relative 'xml_helper'
8
+ require_relative 'exceptions'
8
9
  require_relative 'report/metadata'
9
10
  require_relative 'report/policy'
10
11
  require_relative 'report/record'
@@ -28,6 +29,11 @@ module DMARCator
28
29
  def initialize(xml)
29
30
  @xml = xml
30
31
  @node = Nokogiri::XML(xml)
32
+
33
+ unless @node.errors.empty?
34
+ raise InvalidReport, "invalid DMARC XML report: #{@node.errors.first}"
35
+ end
36
+
31
37
  @node.remove_namespaces!
32
38
  end
33
39
 
@@ -39,7 +45,14 @@ module DMARCator
39
45
  def version
40
46
  @version ||= begin
41
47
  value = get_text('feedback/version')
42
- BigDecimal(value) if value && !value.empty?
48
+
49
+ if value && !value.empty?
50
+ begin
51
+ BigDecimal(value)
52
+ rescue ArgumentError
53
+ nil
54
+ end
55
+ end
43
56
  end
44
57
  end
45
58
 
data/lib/dmarcator/uri.rb CHANGED
@@ -75,6 +75,27 @@ module DMARCator
75
75
  (@unit == other.unit)
76
76
  end
77
77
 
78
+ #
79
+ # Determines if the DMARC URI is equivalent to the other.
80
+ #
81
+ # @param [Object] other
82
+ # the other DMARC URI to compare against.
83
+ #
84
+ # @return [Boolean]
85
+ #
86
+ def eql?(other)
87
+ self == other
88
+ end
89
+
90
+ #
91
+ # Computes a hash value consistent with {#eql?}.
92
+ #
93
+ # @return [Integer]
94
+ #
95
+ def hash
96
+ [self.class, @uri, @size, @unit].hash
97
+ end
98
+
78
99
  #
79
100
  # Converts the DMARC URI back into a String.
80
101
  #
@@ -1,4 +1,4 @@
1
1
  module DMARCator
2
2
  # dmarcator version
3
- VERSION = '0.2.0'
3
+ VERSION = '0.3.1'
4
4
  end
@@ -30,7 +30,12 @@ module DMARCator
30
30
  #
31
31
  def get_timestamp(xpath)
32
32
  value = get_text(xpath)
33
- value && !value.empty? ? Time.at(value.to_i) : nil
33
+ return nil if value.nil? || value.empty?
34
+
35
+ digits = value.strip
36
+ return nil unless digits.match?(/\A\d+\z/)
37
+
38
+ Time.at(digits.to_i)
34
39
  end
35
40
 
36
41
  #
@@ -59,4 +59,70 @@ describe DMARCator do
59
59
  expect { subject.query("foo bar") }.to raise_error(ArgumentError)
60
60
  end
61
61
  end
62
+
63
+ describe ".validate_domain" do
64
+ it "accepts a valid domain" do
65
+ expect(subject.validate_domain("example.com")).to eq("example.com")
66
+ end
67
+
68
+ it "rejects a label longer than 63 octets" do
69
+ expect {
70
+ subject.validate_domain("#{'a' * 64}.com")
71
+ }.to raise_error(ArgumentError)
72
+ end
73
+
74
+ it "accepts a 63-octet label" do
75
+ domain = "#{'a' * 63}.com"
76
+
77
+ expect(subject.validate_domain(domain)).to eq(domain)
78
+ end
79
+
80
+ it "rejects a domain longer than 248 octets" do
81
+ domain = "#{'a' * 62}.#{'a' * 62}.#{'a' * 62}.#{'a' * 60}"
82
+
83
+ expect { subject.validate_domain(domain) }.to raise_error(ArgumentError)
84
+ end
85
+
86
+ it "accepts a domain at the 248-octet limit" do
87
+ domain = "#{'a' * 62}.#{'a' * 62}.#{'a' * 62}.#{'a' * 59}"
88
+
89
+ expect(subject.validate_domain(domain)).to eq(domain)
90
+ end
91
+
92
+ it "rejects a non-string domain" do
93
+ expect { subject.validate_domain(123) }.to raise_error(ArgumentError)
94
+ end
95
+ end
96
+
97
+ describe ".vulnerable_resolv?" do
98
+ it "is true for resolv 0.3.1 and earlier" do
99
+ allow(subject).to receive(:resolv_version).and_return("0.3.1")
100
+
101
+ expect(subject.vulnerable_resolv?).to be(true)
102
+ end
103
+
104
+ it "is false for resolv 0.3.2" do
105
+ allow(subject).to receive(:resolv_version).and_return("0.3.2")
106
+
107
+ expect(subject.vulnerable_resolv?).to be(false)
108
+ end
109
+
110
+ it "is true for resolv 0.4.0 through 0.7.1" do
111
+ allow(subject).to receive(:resolv_version).and_return("0.7.1")
112
+
113
+ expect(subject.vulnerable_resolv?).to be(true)
114
+ end
115
+
116
+ it "is false for resolv 0.7.2 and later" do
117
+ allow(subject).to receive(:resolv_version).and_return("0.7.2")
118
+
119
+ expect(subject.vulnerable_resolv?).to be(false)
120
+ end
121
+
122
+ it "is false when the version is unknown" do
123
+ allow(subject).to receive(:resolv_version).and_return(nil)
124
+
125
+ expect(subject.vulnerable_resolv?).to be(false)
126
+ end
127
+ end
62
128
  end
data/spec/report_spec.rb CHANGED
@@ -23,6 +23,34 @@ RSpec.describe DMARCator::Report do
23
23
  expect(google.version).to be_nil
24
24
  end
25
25
 
26
+ it 'returns nil for a non-numeric report version' do
27
+ report = described_class.new('<feedback><version>abc</version></feedback>')
28
+
29
+ expect(report.version).to be_nil
30
+ end
31
+
32
+ it 'raises InvalidReport for malformed XML' do
33
+ expect {
34
+ described_class.new('this is not xml')
35
+ }.to raise_error(DMARCator::InvalidReport)
36
+ end
37
+
38
+ it 'returns nil for a non-numeric timestamp' do
39
+ report = described_class.new(
40
+ '<feedback><report_metadata><date_range><begin>notanumber</begin></date_range></report_metadata></feedback>'
41
+ )
42
+
43
+ expect(report.metadata.begin_at).to be_nil
44
+ end
45
+
46
+ it 'parses timestamps with surrounding whitespace' do
47
+ report = described_class.new(
48
+ '<feedback><report_metadata><date_range><begin> 1593648000 </begin></date_range></report_metadata></feedback>'
49
+ )
50
+
51
+ expect(report.metadata.begin_at).to eq(Time.at(1_593_648_000))
52
+ end
53
+
26
54
  it 'reads report metadata' do
27
55
  report = described_class.new(xml_report)
28
56
 
data/spec/uri_spec.rb CHANGED
@@ -109,6 +109,52 @@ describe DMARCator::Uri do
109
109
  end
110
110
  end
111
111
 
112
+ describe "#eql?" do
113
+ it "is true when all fields match" do
114
+ other = described_class.new(uri.dup, size, unit)
115
+
116
+ expect(subject.eql?(other)).to be(true)
117
+ end
118
+
119
+ it "is false when the uri differs" do
120
+ other = described_class.new(URI("mailto:other@example.com"), size, unit)
121
+
122
+ expect(subject.eql?(other)).to be(false)
123
+ end
124
+
125
+ it "is false for a different class" do
126
+ expect(subject.eql?(Object.new)).to be(false)
127
+ end
128
+ end
129
+
130
+ describe "#hash" do
131
+ it "is equal for equivalent URIs" do
132
+ other = described_class.new(uri.dup, size, unit)
133
+
134
+ expect(subject.hash).to eq(other.hash)
135
+ end
136
+
137
+ it "differs for non-equivalent URIs" do
138
+ other = described_class.new(URI("mailto:other@example.com"), size, unit)
139
+
140
+ expect(subject.hash).not_to eq(other.hash)
141
+ end
142
+ end
143
+
144
+ describe "Hash/Set semantics" do
145
+ it "deduplicates equivalent URIs" do
146
+ other = described_class.new(uri.dup, size, unit)
147
+
148
+ expect([subject, other].uniq.size).to eq(1)
149
+ end
150
+
151
+ it "treats equivalent URIs as the same Hash key" do
152
+ other = described_class.new(uri.dup, size, unit)
153
+
154
+ expect({ subject => 1 }.key?(other)).to be(true)
155
+ end
156
+ end
157
+
112
158
  describe "#to_s" do
113
159
  context "when only the uri is set" do
114
160
  subject { described_class.new(uri) }
metadata CHANGED
@@ -1,11 +1,10 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dmarcator
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Noah Berman
8
- - Davis Gallinghouse
9
8
  bindir: bin
10
9
  cert_chain: []
11
10
  date: 1980-01-02 00:00:00.000000000 Z
@@ -62,7 +61,6 @@ description: A hardened DMARC record parser and query client, forked from the Tr
62
61
  of Bits 'dmarc' gem.
63
62
  email:
64
63
  - noah@noahberman.org
65
- - davis@trailofbits.com
66
64
  executables: []
67
65
  extensions: []
68
66
  extra_rdoc_files: []
@@ -107,14 +105,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
107
105
  requirements:
108
106
  - - ">="
109
107
  - !ruby/object:Gem::Version
110
- version: 3.0.0
108
+ version: 3.3.13
111
109
  required_rubygems_version: !ruby/object:Gem::Requirement
112
110
  requirements:
113
111
  - - ">="
114
112
  - !ruby/object:Gem::Version
115
113
  version: '0'
116
114
  requirements: []
117
- rubygems_version: 4.0.10
115
+ rubygems_version: 4.0.23
118
116
  specification_version: 4
119
117
  summary: DMARC record parser and query client
120
118
  test_files: