|
1 | 1 | # XXX TypeErrors on calling handlers, or on bad return values from a |
2 | 2 | # handler, are obscure and unhelpful. |
3 | 3 |
|
| 4 | +importabc |
| 5 | +importfunctools |
4 | 6 | importos |
| 7 | +importre |
5 | 8 | importsys |
6 | 9 | importsysconfig |
7 | 10 | importtextwrap |
8 | 11 | importunittest |
9 | 12 | importtraceback |
10 | 13 | fromioimportBytesIO |
11 | 14 | fromtestimportsupport |
12 | | -fromtest.supportimportos_helper |
| 15 | +fromtest.supportimportimport_helper,os_helper |
13 | 16 |
|
14 | 17 | fromxml.parsersimportexpat |
15 | 18 | fromxml.parsers.expatimporterrors |
@@ -863,5 +866,199 @@ def start_element(name, _): |
863 | 866 | self.assertEqual(started, ['doc']) |
864 | 867 |
|
865 | 868 |
|
| 869 | +classAttackProtectionTestBase(abc.ABC): |
| 870 | +""" |
| 871 | + Base class for testing protections against XML payloads with |
| 872 | + disproportionate amplification. |
| 873 | +
|
| 874 | + The protections being tested should detect and prevent attacks |
| 875 | + that leverage disproportionate amplification from small inputs. |
| 876 | + """ |
| 877 | + |
| 878 | +@staticmethod |
| 879 | +defexponential_expansion_payload(*,nrows,ncols,text='.'): |
| 880 | +"""Create a billion laughs attack payload. |
| 881 | +
|
| 882 | + Be careful: the number of total items is pow(n, k), thereby |
| 883 | + requiring at least pow(ncols, nrows) * sizeof(text) memory! |
| 884 | + """ |
| 885 | +template=textwrap.dedent(f"""\ |
| 886 | + <?xml version="1.0"?> |
| 887 | + <!DOCTYPE doc [ |
| 888 | + <!ENTITY row0 "{text}"> |
| 889 | + <!ELEMENT doc (#PCDATA)> |
| 890 | + {{body}} |
| 891 | + ]> |
| 892 | + <doc>&row{nrows};</doc> |
| 893 | + """).rstrip() |
| 894 | + |
| 895 | +body='\n'.join( |
| 896 | +f'<!ENTITY row{i+1} "{f"&row{i};"*ncols}">' |
| 897 | +foriinrange(nrows) |
| 898 | + ) |
| 899 | +body=textwrap.indent(body,' '*4) |
| 900 | +returntemplate.format(body=body) |
| 901 | + |
| 902 | +deftest_payload_generation(self): |
| 903 | +# self-test for exponential_expansion_payload() |
| 904 | +payload=self.exponential_expansion_payload(nrows=2,ncols=3) |
| 905 | +self.assertEqual(payload,textwrap.dedent("""\ |
| 906 | + <?xml version="1.0"?> |
| 907 | + <!DOCTYPE doc [ |
| 908 | + <!ENTITY row0 "."> |
| 909 | + <!ELEMENT doc (#PCDATA)> |
| 910 | + <!ENTITY row1 "&row0;&row0;&row0;"> |
| 911 | + <!ENTITY row2 "&row1;&row1;&row1;"> |
| 912 | + ]> |
| 913 | + <doc>&row2;</doc> |
| 914 | + """).rstrip()) |
| 915 | + |
| 916 | +defassert_root_parser_failure(self,func,/,*args,**kwargs): |
| 917 | +"""Check that func(*args, **kwargs) is invalid for a sub-parser.""" |
| 918 | +msg="parser must be a root parser" |
| 919 | +self.assertRaisesRegex(expat.ExpatError,msg,func,*args,**kwargs) |
| 920 | + |
| 921 | +@abc.abstractmethod |
| 922 | +defassert_rejected(self,func,/,*args,**kwargs): |
| 923 | +"""Assert that func(*args, **kwargs) triggers the attack protection. |
| 924 | +
|
| 925 | + Note: this method must ensure that the attack protection being tested |
| 926 | + is the one that is actually triggered at runtime, e.g., by matching |
| 927 | + the exact error message. |
| 928 | + """ |
| 929 | + |
| 930 | +@abc.abstractmethod |
| 931 | +defset_activation_threshold(self,parser,threshold): |
| 932 | +"""Set the activation threshold for the tested protection.""" |
| 933 | + |
| 934 | +@abc.abstractmethod |
| 935 | +defset_maximum_amplification(self,parser,max_factor): |
| 936 | +"""Set the maximum amplification factor for the tested protection.""" |
| 937 | + |
| 938 | +@abc.abstractmethod |
| 939 | +deftest_set_activation_threshold__threshold_reached(self): |
| 940 | +"""Test when the activation threshold is exceeded.""" |
| 941 | + |
| 942 | +@abc.abstractmethod |
| 943 | +deftest_set_activation_threshold__threshold_not_reached(self): |
| 944 | +"""Test when the activation threshold is not exceeded.""" |
| 945 | + |
| 946 | +deftest_set_activation_threshold__invalid_threshold_type(self): |
| 947 | +parser=expat.ParserCreate() |
| 948 | +setter=functools.partial(self.set_activation_threshold,parser) |
| 949 | + |
| 950 | +self.assertRaises(TypeError,setter,1.0) |
| 951 | +self.assertRaises(TypeError,setter,-1.5) |
| 952 | +self.assertRaises(ValueError,setter,-5) |
| 953 | + |
| 954 | +deftest_set_activation_threshold__invalid_threshold_range(self): |
| 955 | +_testcapi=import_helper.import_module("_testcapi") |
| 956 | +parser=expat.ParserCreate() |
| 957 | +setter=functools.partial(self.set_activation_threshold,parser) |
| 958 | + |
| 959 | +self.assertRaises(OverflowError,setter,_testcapi.ULLONG_MAX+1) |
| 960 | + |
| 961 | +deftest_set_activation_threshold__fail_for_subparser(self): |
| 962 | +parser=expat.ParserCreate() |
| 963 | +subparser=parser.ExternalEntityParserCreate(None) |
| 964 | +setter=functools.partial(self.set_activation_threshold,subparser) |
| 965 | +self.assert_root_parser_failure(setter,12345) |
| 966 | + |
| 967 | +@abc.abstractmethod |
| 968 | +deftest_set_maximum_amplification__amplification_exceeded(self): |
| 969 | +"""Test when the amplification factor is exceeded.""" |
| 970 | + |
| 971 | +@abc.abstractmethod |
| 972 | +deftest_set_maximum_amplification__amplification_not_exceeded(self): |
| 973 | +"""Test when the amplification factor is not exceeded.""" |
| 974 | + |
| 975 | +deftest_set_maximum_amplification__infinity(self): |
| 976 | +inf=float('inf')# an 'inf' threshold is allowed by Expat |
| 977 | +parser=expat.ParserCreate() |
| 978 | +self.assertIsNone(self.set_maximum_amplification(parser,inf)) |
| 979 | + |
| 980 | +deftest_set_maximum_amplification__invalid_max_factor_type(self): |
| 981 | +parser=expat.ParserCreate() |
| 982 | +setter=functools.partial(self.set_maximum_amplification,parser) |
| 983 | + |
| 984 | +self.assertRaises(TypeError,setter,None) |
| 985 | +self.assertRaises(TypeError,setter,'abc') |
| 986 | + |
| 987 | +deftest_set_maximum_amplification__invalid_max_factor_range(self): |
| 988 | +parser=expat.ParserCreate() |
| 989 | +setter=functools.partial(self.set_maximum_amplification,parser) |
| 990 | + |
| 991 | +msg=re.escape("'max_factor' must be at least 1.0") |
| 992 | +self.assertRaisesRegex(expat.ExpatError,msg,setter,float('nan')) |
| 993 | +self.assertRaisesRegex(expat.ExpatError,msg,setter,0.99) |
| 994 | + |
| 995 | +deftest_set_maximum_amplification__fail_for_subparser(self): |
| 996 | +parser=expat.ParserCreate() |
| 997 | +subparser=parser.ExternalEntityParserCreate(None) |
| 998 | +setter=functools.partial(self.set_maximum_amplification,subparser) |
| 999 | +self.assert_root_parser_failure(setter,123.45) |
| 1000 | + |
| 1001 | + |
| 1002 | +@unittest.skipIf(expat.version_info< (2,7,2),"requires Expat >= 2.7.2") |
| 1003 | +classMemoryProtectionTest(AttackProtectionTestBase,unittest.TestCase): |
| 1004 | + |
| 1005 | +# NOTE: with the default Expat configuration, the billion laughs protection |
| 1006 | +# may hit before the allocation limiter if exponential_expansion_payload() |
| 1007 | +# is not carefully parametrized. As such, the payloads should be chosen so |
| 1008 | +# that either the allocation limiter is hit before other protections are |
| 1009 | +# triggered or no protection at all is triggered. |
| 1010 | + |
| 1011 | +defassert_rejected(self,func,/,*args,**kwargs): |
| 1012 | +"""Check that func(*args, **kwargs) hits the allocation limit.""" |
| 1013 | +msg=r"out of memory: line \d+, column \d+" |
| 1014 | +self.assertRaisesRegex(expat.ExpatError,msg,func,*args,**kwargs) |
| 1015 | + |
| 1016 | +defset_activation_threshold(self,parser,threshold): |
| 1017 | +returnparser.SetAllocTrackerActivationThreshold(threshold) |
| 1018 | + |
| 1019 | +defset_maximum_amplification(self,parser,max_factor): |
| 1020 | +returnparser.SetAllocTrackerMaximumAmplification(max_factor) |
| 1021 | + |
| 1022 | +deftest_set_activation_threshold__threshold_reached(self): |
| 1023 | +parser=expat.ParserCreate() |
| 1024 | +# Choose a threshold expected to be always reached. |
| 1025 | +self.set_activation_threshold(parser,3) |
| 1026 | +# Check that the threshold is reached by choosing a small factor |
| 1027 | +# and a payload whose peak amplification factor exceeds it. |
| 1028 | +self.assertIsNone(self.set_maximum_amplification(parser,1.0)) |
| 1029 | +payload=self.exponential_expansion_payload(ncols=10,nrows=4) |
| 1030 | +self.assert_rejected(parser.Parse,payload,True) |
| 1031 | + |
| 1032 | +deftest_set_activation_threshold__threshold_not_reached(self): |
| 1033 | +parser=expat.ParserCreate() |
| 1034 | +# Choose a threshold expected to be never reached. |
| 1035 | +self.set_activation_threshold(parser,pow(10,5)) |
| 1036 | +# Check that the threshold is reached by choosing a small factor |
| 1037 | +# and a payload whose peak amplification factor exceeds it. |
| 1038 | +self.assertIsNone(self.set_maximum_amplification(parser,1.0)) |
| 1039 | +payload=self.exponential_expansion_payload(ncols=10,nrows=4) |
| 1040 | +self.assertIsNotNone(parser.Parse(payload,True)) |
| 1041 | + |
| 1042 | +deftest_set_maximum_amplification__amplification_exceeded(self): |
| 1043 | +parser=expat.ParserCreate() |
| 1044 | +# Unconditionally enable maximum activation factor. |
| 1045 | +self.set_activation_threshold(parser,0) |
| 1046 | +# Choose a max amplification factor expected to always be exceeded. |
| 1047 | +self.assertIsNone(self.set_maximum_amplification(parser,1.0)) |
| 1048 | +# Craft a payload for which the peak amplification factor is > 1.0. |
| 1049 | +payload=self.exponential_expansion_payload(ncols=1,nrows=2) |
| 1050 | +self.assert_rejected(parser.Parse,payload,True) |
| 1051 | + |
| 1052 | +deftest_set_maximum_amplification__amplification_not_exceeded(self): |
| 1053 | +parser=expat.ParserCreate() |
| 1054 | +# Unconditionally enable maximum activation factor. |
| 1055 | +self.set_activation_threshold(parser,0) |
| 1056 | +# Choose a max amplification factor expected to never be exceeded. |
| 1057 | +self.assertIsNone(self.set_maximum_amplification(parser,1e4)) |
| 1058 | +# Craft a payload for which the peak amplification factor is < 1e4. |
| 1059 | +payload=self.exponential_expansion_payload(ncols=1,nrows=2) |
| 1060 | +self.assertIsNotNone(parser.Parse(payload,True)) |
| 1061 | + |
| 1062 | + |
866 | 1063 | if__name__=="__main__": |
867 | 1064 | unittest.main() |