diff --git a/ChangeLog.md b/ChangeLog.md index d784805..59a6ef1 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -6,9 +6,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] +## [v1.0.1] - 2026-09-08 + ### Fixed * Typo in Pod (thanks @stefanhaerter) +* Validate the `format` argument against a list of known formatters before + using it to build and load a class name, preventing code injection via a + malicious format string ## [v1.0.0] - 2025-11-17 diff --git a/lib/XML/Feed.pm b/lib/XML/Feed.pm index d284e21..4e9c756 100644 --- a/lib/XML/Feed.pm +++ b/lib/XML/Feed.pm @@ -13,11 +13,16 @@ use Module::Pluggable search_path => "XML::Feed::Format", require => 1, sub_name => 'formatters'; -our $VERSION = 'v1.0.0'; +our $VERSION = '1.0.1'; our $MULTIPLE_ENCLOSURES = 0; our @formatters; +our %formatters; BEGIN { @formatters = __PACKAGE__->formatters; + for my $formatter (@formatters) { + (my $name = $formatter) =~ s/^XML::Feed::Format:://; + $formatters{$name} = 1; + } } sub _parse_args { @@ -57,6 +62,8 @@ sub new { $args = $class->_parse_args(pop @_); } + Carp::croak("Unsupported format $format: not a recognised feed format") + unless $formatters{$format}; my $format_class = 'XML::Feed::Format::' . $format; eval "use $format_class"; Carp::croak("Unsupported format $format: $@") if $@; @@ -114,6 +121,8 @@ sub parse { } my $format_class = join '::', __PACKAGE__, "Format", $format; + return $class->error("Unsupported format $format: not a recognised feed format") + unless $formatters{$format}; eval "use $format_class"; return $class->error("Unsupported format $format: $@") if $@; bless $feed, $format_class; diff --git a/lib/XML/Feed/Content.pm b/lib/XML/Feed/Content.pm index a4b000a..305ba9c 100644 --- a/lib/XML/Feed/Content.pm +++ b/lib/XML/Feed/Content.pm @@ -2,7 +2,7 @@ package XML::Feed::Content; use strict; use warnings; -our $VERSION = '0.65'; +our $VERSION = '1.0.1'; use base qw( Class::ErrorHandler ); diff --git a/lib/XML/Feed/Enclosure.pm b/lib/XML/Feed/Enclosure.pm index 3c69a35..3f6545a 100644 --- a/lib/XML/Feed/Enclosure.pm +++ b/lib/XML/Feed/Enclosure.pm @@ -2,7 +2,7 @@ package XML::Feed::Enclosure; use strict; use warnings; -our $VERSION = '0.65'; +our $VERSION = '1.0.1'; use base qw( Class::ErrorHandler ); diff --git a/lib/XML/Feed/Entry.pm b/lib/XML/Feed/Entry.pm index 23e06d6..584d5c4 100644 --- a/lib/XML/Feed/Entry.pm +++ b/lib/XML/Feed/Entry.pm @@ -3,7 +3,7 @@ use strict; use warnings; use v5.10; -our $VERSION = '0.65'; +our $VERSION = '1.0.1'; use base qw( Class::ErrorHandler ); @@ -11,6 +11,8 @@ use Scalar::Util qw( blessed ); use Carp; +use XML::Feed (); + sub wrap { my $class = shift; my($item) = @_; @@ -23,6 +25,8 @@ sub new { my $class = shift; my($format) = @_; $format //= 'Atom'; + Carp::croak("Unsupported format $format: not a recognised feed format") + unless $XML::Feed::formatters{$format}; my $format_class = 'XML::Feed::Format::' . $format; eval "use $format_class"; Carp::croak("Unsupported format $format: $@") if $@; diff --git a/lib/XML/Feed/Entry/Format/Atom.pm b/lib/XML/Feed/Entry/Format/Atom.pm index 92b80d8..d059ee6 100644 --- a/lib/XML/Feed/Entry/Format/Atom.pm +++ b/lib/XML/Feed/Entry/Format/Atom.pm @@ -3,7 +3,7 @@ use strict; use warnings; use v5.10; -our $VERSION = '0.65'; +our $VERSION = '1.0.1'; use base qw( XML::Feed::Entry ); use XML::Feed::Content; diff --git a/lib/XML/Feed/Entry/Format/RSS.pm b/lib/XML/Feed/Entry/Format/RSS.pm index 2901abb..403c2fa 100644 --- a/lib/XML/Feed/Entry/Format/RSS.pm +++ b/lib/XML/Feed/Entry/Format/RSS.pm @@ -3,7 +3,7 @@ use strict; use warnings; use v5.10; -our $VERSION = '0.65'; +our $VERSION = '1.0.1'; sub format { 'RSS ' . $_[0]->{'_version'} } diff --git a/lib/XML/Feed/Format/Atom.pm b/lib/XML/Feed/Format/Atom.pm index 89a7f73..846a25d 100644 --- a/lib/XML/Feed/Format/Atom.pm +++ b/lib/XML/Feed/Format/Atom.pm @@ -3,7 +3,7 @@ use strict; use warnings; use v5.10; -our $VERSION = '0.65'; +our $VERSION = '1.0.1'; use base qw( XML::Feed ); use XML::Atom::Feed; diff --git a/lib/XML/Feed/Format/RSS.pm b/lib/XML/Feed/Format/RSS.pm index 3fabaa2..a2a0d56 100644 --- a/lib/XML/Feed/Format/RSS.pm +++ b/lib/XML/Feed/Format/RSS.pm @@ -3,7 +3,7 @@ use strict; use warnings; use v5.10; -our $VERSION = '0.65'; +our $VERSION = '1.0.1'; use base qw( XML::Feed ); use DateTime::Format::Mail; diff --git a/lib/XML/Feed/Util.pm b/lib/XML/Feed/Util.pm index 38ded02..ef7fbfc 100644 --- a/lib/XML/Feed/Util.pm +++ b/lib/XML/Feed/Util.pm @@ -2,7 +2,7 @@ package XML::Feed::Util; use strict; use warnings; -our $VERSION = '0.65'; +our $VERSION = '1.0.1'; use base qw( Exporter ); use DateTime::Format::Flexible; diff --git a/t/32-format-injection.t b/t/32-format-injection.t new file mode 100644 index 0000000..d03dfa7 --- /dev/null +++ b/t/32-format-injection.t @@ -0,0 +1,49 @@ +use strict; +use warnings; +use Test::More tests => 11; + +use XML::Feed; +use XML::Feed::Entry; + +# %formatters should be populated with the known, safe format names. +ok exists $XML::Feed::formatters{Atom}, 'Atom is a known formatter'; +ok exists $XML::Feed::formatters{RSS}, 'RSS is a known formatter'; + +my $marker = '/tmp/xml-feed-format-injection-test'; +unlink $marker; + +my $malicious = qq{Atom; system('touch $marker')}; + +eval { + XML::Feed->new($malicious); +}; +like $@, qr{Unsupported format \Q$malicious\E:}, 'XML::Feed->new rejects an unrecognised format'; +ok !-e $marker, 'XML::Feed->new did not execute injected code'; + +eval { + XML::Feed::Entry->new($malicious); +}; +like $@, qr{Unsupported format \Q$malicious\E:}, 'XML::Feed::Entry->new rejects an unrecognised format'; +ok !-e $marker, 'XML::Feed::Entry->new did not execute injected code'; + +{ + my $result = XML::Feed->parse(\<<'XML', $malicious); + +Test +XML + ok !defined($result), 'XML::Feed->parse rejects an unrecognised format'; + like( XML::Feed->errstr, qr{Unsupported format \Q$malicious\E:}, 'XML::Feed->parse error message' ); + ok !-e $marker, 'XML::Feed->parse did not execute injected code'; +} + +unlink $marker; + +eval { + XML::Feed->new('Nofeed;semicolon'); +}; +like $@, qr{Unsupported format Nofeed;semicolon:}, 'XML::Feed->new rejects format names with a semicolon'; + +eval { + XML::Feed::Entry->new('Nofeed;semicolon'); +}; +like $@, qr{Unsupported format Nofeed;semicolon:}, 'XML::Feed::Entry->new rejects format names with a semicolon';