Merge branch 'fix_arbitrary_code_execution' into develop
This commit is contained in:
		
							
								
								
									
										64
									
								
								owrx/dsp.py
									
									
									
									
									
								
							
							
						
						
									
										64
									
								
								owrx/dsp.py
									
									
									
									
									
								
							| @@ -4,16 +4,26 @@ from owrx.js8 import Js8Parser | ||||
| from owrx.aprs import AprsParser | ||||
| from owrx.pocsag import PocsagParser | ||||
| from owrx.source import SdrSource, SdrSourceEventClient | ||||
| from owrx.property import PropertyStack, PropertyLayer | ||||
| from owrx.property import PropertyStack, PropertyLayer, PropertyValidator | ||||
| from owrx.property.validators import OrValidator, RegexValidator, BoolValidator | ||||
| from owrx.modes import Modes | ||||
| from csdr import csdr | ||||
| import threading | ||||
| import re | ||||
|  | ||||
| import logging | ||||
|  | ||||
| logger = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| class ModulationValidator(OrValidator): | ||||
|     """ | ||||
|     This validator only allows alphanumeric characters and numbers, but no spaces or special characters | ||||
|     """ | ||||
|     def __init__(self): | ||||
|         super().__init__(BoolValidator(), RegexValidator(re.compile("^[a-z0-9]+$"))) | ||||
|  | ||||
|  | ||||
| class DspManager(csdr.output, SdrSourceEventClient): | ||||
|     def __init__(self, handler, sdrSource): | ||||
|         self.handler = handler | ||||
| @@ -27,22 +37,42 @@ class DspManager(csdr.output, SdrSourceEventClient): | ||||
|         } | ||||
|  | ||||
|         self.props = PropertyStack() | ||||
|  | ||||
|         # local demodulator properties not forwarded to the sdr | ||||
|         self.props.addLayer( | ||||
|             0, | ||||
|             PropertyLayer().filter( | ||||
|                 "output_rate", | ||||
|                 "hd_output_rate", | ||||
|                 "squelch_level", | ||||
|                 "secondary_mod", | ||||
|                 "low_cut", | ||||
|                 "high_cut", | ||||
|                 "offset_freq", | ||||
|                 "mod", | ||||
|                 "secondary_offset_freq", | ||||
|                 "dmr_filter", | ||||
|             ), | ||||
|         ) | ||||
|         # ensure strict validation since these can be set from the client | ||||
|         # and are used to build executable commands | ||||
|         validators = { | ||||
|             "output_rate": "int", | ||||
|             "hd_output_rate": "int", | ||||
|             "squelch_level": "num", | ||||
|             "secondary_mod": ModulationValidator(), | ||||
|             "low_cut": "num", | ||||
|             "high_cut": "num", | ||||
|             "offset_freq": "int", | ||||
|             "mod": ModulationValidator(), | ||||
|             "secondary_offset_freq": "int", | ||||
|             "dmr_filter": "int", | ||||
|         } | ||||
|         self.localProps = PropertyValidator(PropertyLayer().filter(*validators.keys()), validators) | ||||
|  | ||||
|         self.props.addLayer(0, self.localProps) | ||||
|         # ensure strict validation since these can be set from the client | ||||
|         # and are used to build executable commands | ||||
|         validators = { | ||||
|             "output_rate": "int", | ||||
|             "hd_output_rate": "int", | ||||
|             "squelch_level": "num", | ||||
|             "secondary_mod": ModulationValidator(), | ||||
|             "low_cut": "num", | ||||
|             "high_cut": "num", | ||||
|             "offset_freq": "int", | ||||
|             "mod": ModulationValidator(), | ||||
|             "secondary_offset_freq": "int", | ||||
|             "dmr_filter": "int", | ||||
|         } | ||||
|         self.localProps = PropertyValidator(PropertyLayer().filter(*validators.keys()), validators) | ||||
|  | ||||
|         self.props.addLayer(0, self.localProps) | ||||
|         # properties that we inherit from the sdr | ||||
|         self.props.addLayer( | ||||
|             1, | ||||
| @@ -188,7 +218,7 @@ class DspManager(csdr.output, SdrSourceEventClient): | ||||
|             self.setProperty(k, v) | ||||
|  | ||||
|     def setProperty(self, prop, value): | ||||
|         self.props[prop] = value | ||||
|         self.localProps[prop] = value | ||||
|  | ||||
|     def getClientClass(self): | ||||
|         return SdrSource.CLIENT_USER | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| from abc import ABC, abstractmethod | ||||
| from owrx.property.validators import Validator | ||||
| import logging | ||||
|  | ||||
| logger = logging.getLogger(__name__) | ||||
| @@ -154,6 +155,50 @@ class PropertyFilter(PropertyManager): | ||||
|         return [k for k in self.pm.keys() if k in self.props] | ||||
|  | ||||
|  | ||||
| class PropertyValidationError(Exception): | ||||
|     def __init__(self, key, value): | ||||
|         super().__init__('Invalid value for property "{key}": "{value}"'.format(key=key, value=str(value))) | ||||
|  | ||||
|  | ||||
| class PropertyValidator(PropertyManager): | ||||
|     def __init__(self, pm: PropertyManager, validators=None): | ||||
|         self.pm = pm | ||||
|         self.pm.wire(self._fireCallbacks) | ||||
|         if validators is None: | ||||
|             self.validators = {} | ||||
|         else: | ||||
|             self.validators = {k: Validator.of(v) for k, v in validators.items()} | ||||
|         super().__init__() | ||||
|  | ||||
|     def validate(self, key, value): | ||||
|         if key not in self.validators: | ||||
|             return | ||||
|         if not self.validators[key].isValid(value): | ||||
|             raise PropertyValidationError(key, value) | ||||
|  | ||||
|     def setValidator(self, key, validator): | ||||
|         self.validators[key] = Validator.of(validator) | ||||
|  | ||||
|     def __getitem__(self, item): | ||||
|         return self.pm.__getitem__(item) | ||||
|  | ||||
|     def __setitem__(self, key, value): | ||||
|         self.validate(key, value) | ||||
|         return self.pm.__setitem__(key, value) | ||||
|  | ||||
|     def __contains__(self, item): | ||||
|         return self.pm.__contains__(item) | ||||
|  | ||||
|     def __dict__(self): | ||||
|         return self.pm.__dict__() | ||||
|  | ||||
|     def __delitem__(self, key): | ||||
|         return self.pm.__delitem__(key) | ||||
|  | ||||
|     def keys(self): | ||||
|         return self.pm.keys() | ||||
|  | ||||
|  | ||||
| class PropertyStack(PropertyManager): | ||||
|     def __init__(self): | ||||
|         super().__init__() | ||||
|   | ||||
							
								
								
									
										98
									
								
								owrx/property/validators.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								owrx/property/validators.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,98 @@ | ||||
| from abc import ABC, abstractmethod | ||||
| from functools import reduce | ||||
| from operator import or_ | ||||
| import re | ||||
|  | ||||
|  | ||||
| class ValidatorException(Exception): | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class Validator(ABC): | ||||
|     @staticmethod | ||||
|     def of(x): | ||||
|         if isinstance(x, Validator): | ||||
|             return x | ||||
|         if callable(x): | ||||
|             return LambdaValidator(x) | ||||
|         if x in validator_types: | ||||
|             return validator_types[x]() | ||||
|         raise ValidatorException("Cannot create validator") | ||||
|  | ||||
|     @abstractmethod | ||||
|     def isValid(self, value): | ||||
|         pass | ||||
|  | ||||
|  | ||||
| class LambdaValidator(Validator): | ||||
|     def __init__(self, c): | ||||
|         self.callable = c | ||||
|  | ||||
|     def isValid(self, value): | ||||
|         return self.callable(value) | ||||
|  | ||||
|  | ||||
| class TypeValidator(Validator): | ||||
|     def __init__(self, type): | ||||
|         self.type = type | ||||
|         super().__init__() | ||||
|  | ||||
|     def isValid(self, value): | ||||
|         return isinstance(value, self.type) | ||||
|  | ||||
|  | ||||
| class IntegerValidator(TypeValidator): | ||||
|     def __init__(self): | ||||
|         super().__init__(int) | ||||
|  | ||||
|  | ||||
| class FloatValidator(TypeValidator): | ||||
|     def __init__(self): | ||||
|         super().__init__(float) | ||||
|  | ||||
|  | ||||
| class StringValidator(TypeValidator): | ||||
|     def __init__(self): | ||||
|         super().__init__(str) | ||||
|  | ||||
|  | ||||
| class BoolValidator(TypeValidator): | ||||
|     def __init__(self): | ||||
|         super().__init__(bool) | ||||
|  | ||||
|  | ||||
| class OrValidator(Validator): | ||||
|     def __init__(self, *validators): | ||||
|         self.validators = validators | ||||
|         super().__init__() | ||||
|  | ||||
|     def isValid(self, value): | ||||
|         return reduce( | ||||
|             or_, | ||||
|             [v.isValid(value) for v in self.validators], | ||||
|             False | ||||
|         ) | ||||
|  | ||||
|  | ||||
| class NumberValidator(OrValidator): | ||||
|     def __init__(self): | ||||
|         super().__init__(IntegerValidator(), FloatValidator()) | ||||
|  | ||||
|  | ||||
| class RegexValidator(StringValidator): | ||||
|     def __init__(self, regex: re.Pattern): | ||||
|         self.regex = regex | ||||
|         super().__init__() | ||||
|  | ||||
|     def isValid(self, value): | ||||
|         return super().isValid(value) and self.regex.match(value) is not None | ||||
|  | ||||
|  | ||||
| validator_types = { | ||||
|     "string": StringValidator, | ||||
|     "str": StringValidator, | ||||
|     "integer": IntegerValidator, | ||||
|     "int": IntegerValidator, | ||||
|     "number": NumberValidator, | ||||
|     "num": NumberValidator, | ||||
| } | ||||
| @@ -48,3 +48,11 @@ class PropertyFilterTest(TestCase): | ||||
|         pf["testkey"] = "new value" | ||||
|         self.assertEqual(pm["testkey"], "new value") | ||||
|         self.assertEqual(pf["testkey"], "new value") | ||||
|  | ||||
|     def testRejectsWrite(self): | ||||
|         pm = PropertyLayer() | ||||
|         pm["testkey"] = "old value" | ||||
|         pf = PropertyFilter(pm, "otherkey") | ||||
|         with self.assertRaises(KeyError): | ||||
|             pf["testkey"] = "new value" | ||||
|         self.assertEqual(pm["testkey"], "old value") | ||||
|   | ||||
							
								
								
									
										37
									
								
								test/property/test_property_validator.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								test/property/test_property_validator.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | ||||
| from unittest import TestCase | ||||
| from owrx.property import PropertyLayer, PropertyValidator, PropertyValidationError | ||||
| from owrx.property.validators import NumberValidator, StringValidator | ||||
|  | ||||
|  | ||||
| class PropertyValidatorTest(TestCase): | ||||
|     def testPassesUnvalidated(self): | ||||
|         pm = PropertyLayer() | ||||
|         pv = PropertyValidator(pm) | ||||
|         pv["testkey"] = "testvalue" | ||||
|         self.assertEqual(pv["testkey"], "testvalue") | ||||
|         self.assertEqual(pm["testkey"], "testvalue") | ||||
|  | ||||
|     def testPassesValidValue(self): | ||||
|         pv = PropertyValidator(PropertyLayer(), {"testkey": NumberValidator()}) | ||||
|         pv["testkey"] = 42 | ||||
|         self.assertEqual(pv["testkey"], 42) | ||||
|  | ||||
|     def testThrowsErrorOnInvalidValue(self): | ||||
|         pv = PropertyValidator(PropertyLayer(), {"testkey": NumberValidator()}) | ||||
|         with self.assertRaises(PropertyValidationError): | ||||
|             pv["testkey"] = "text" | ||||
|  | ||||
|     def testSetValidator(self): | ||||
|         pv = PropertyValidator(PropertyLayer()) | ||||
|         pv.setValidator("testkey", NumberValidator()) | ||||
|         with self.assertRaises(PropertyValidationError): | ||||
|             pv["testkey"] = "text" | ||||
|  | ||||
|     def testUpdateValidator(self): | ||||
|         pv = PropertyValidator(PropertyLayer(), {"testkey": StringValidator()}) | ||||
|         # this should pass | ||||
|         pv["testkey"] = "text" | ||||
|         pv.setValidator("testkey", NumberValidator()) | ||||
|         # this should raise | ||||
|         with self.assertRaises(PropertyValidationError): | ||||
|             pv["testkey"] = "text" | ||||
							
								
								
									
										0
									
								
								test/property/validators/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								test/property/validators/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										17
									
								
								test/property/validators/test_bool_validator.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								test/property/validators/test_bool_validator.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| from unittest import TestCase | ||||
| from owrx.property.validators import BoolValidator | ||||
|  | ||||
|  | ||||
| class NumberValidatorTest(TestCase): | ||||
|     def testPassesNumbers(self): | ||||
|         validator = BoolValidator() | ||||
|         self.assertTrue(validator.isValid(True)) | ||||
|         self.assertTrue(validator.isValid(False)) | ||||
|  | ||||
|     def testDoesntPassOthers(self): | ||||
|         validator = BoolValidator() | ||||
|         self.assertFalse(validator.isValid(123)) | ||||
|         self.assertFalse(validator.isValid(-2)) | ||||
|         self.assertFalse(validator.isValid(.5)) | ||||
|         self.assertFalse(validator.isValid("text")) | ||||
|         self.assertFalse(validator.isValid(object())) | ||||
							
								
								
									
										15
									
								
								test/property/validators/test_float_validator.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								test/property/validators/test_float_validator.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| from unittest import TestCase | ||||
| from owrx.property.validators import FloatValidator | ||||
|  | ||||
|  | ||||
| class FloatValidatorTest(TestCase): | ||||
|     def testPassesNumbers(self): | ||||
|         validator = FloatValidator() | ||||
|         self.assertTrue(validator.isValid(.5)) | ||||
|  | ||||
|     def testDoesntPassOthers(self): | ||||
|         validator = FloatValidator() | ||||
|         self.assertFalse(validator.isValid(123)) | ||||
|         self.assertFalse(validator.isValid(-2)) | ||||
|         self.assertFalse(validator.isValid("text")) | ||||
|         self.assertFalse(validator.isValid(object())) | ||||
							
								
								
									
										15
									
								
								test/property/validators/test_integer_validator.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								test/property/validators/test_integer_validator.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| from unittest import TestCase | ||||
| from owrx.property.validators import IntegerValidator | ||||
|  | ||||
|  | ||||
| class IntegerValidatorTest(TestCase): | ||||
|     def testPassesIntegers(self): | ||||
|         validator = IntegerValidator() | ||||
|         self.assertTrue(validator.isValid(123)) | ||||
|         self.assertTrue(validator.isValid(-2)) | ||||
|  | ||||
|     def testDoesntPassOthers(self): | ||||
|         validator = IntegerValidator() | ||||
|         self.assertFalse(validator.isValid(.5)) | ||||
|         self.assertFalse(validator.isValid("text")) | ||||
|         self.assertFalse(validator.isValid(object())) | ||||
							
								
								
									
										21
									
								
								test/property/validators/test_lambda_validator.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								test/property/validators/test_lambda_validator.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| from unittest import TestCase | ||||
| from unittest.mock import Mock | ||||
| from owrx.property.validators import LambdaValidator | ||||
|  | ||||
|  | ||||
| class LambdaValidatorTest(TestCase): | ||||
|     def testPassesValue(self): | ||||
|         mock = Mock() | ||||
|         validator = LambdaValidator(mock.method) | ||||
|         validator.isValid("test") | ||||
|         mock.method.assert_called_once_with("test") | ||||
|  | ||||
|     def testReturnsTrue(self): | ||||
|         validator = LambdaValidator(lambda x: True) | ||||
|         self.assertTrue(validator.isValid("any value")) | ||||
|         self.assertTrue(validator.isValid(3.1415926)) | ||||
|  | ||||
|     def testReturnsFalse(self): | ||||
|         validator = LambdaValidator(lambda x: False) | ||||
|         self.assertFalse(validator.isValid("any value")) | ||||
|         self.assertFalse(validator.isValid(42)) | ||||
							
								
								
									
										18
									
								
								test/property/validators/test_number_validator.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								test/property/validators/test_number_validator.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| from unittest import TestCase | ||||
| from owrx.property.validators import NumberValidator | ||||
|  | ||||
|  | ||||
| class NumberValidatorTest(TestCase): | ||||
|     def testPassesNumbers(self): | ||||
|         validator = NumberValidator() | ||||
|         self.assertTrue(validator.isValid(123)) | ||||
|         self.assertTrue(validator.isValid(-2)) | ||||
|         self.assertTrue(validator.isValid(.5)) | ||||
|  | ||||
|     def testDoesntPassOthers(self): | ||||
|         validator = NumberValidator() | ||||
|         # bool is a subclass of int, so it passes this test. | ||||
|         # not sure if we need to be more specific or if this is alright. | ||||
|         # self.assertFalse(validator.isValid(True)) | ||||
|         self.assertFalse(validator.isValid("text")) | ||||
|         self.assertFalse(validator.isValid(object())) | ||||
							
								
								
									
										17
									
								
								test/property/validators/test_or_validator.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								test/property/validators/test_or_validator.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| from unittest import TestCase | ||||
| from owrx.property.validators import OrValidator, IntegerValidator, StringValidator | ||||
|  | ||||
|  | ||||
| class OrValidatorTest(TestCase): | ||||
|     def testPassesAnyValidators(self): | ||||
|         validator = OrValidator(IntegerValidator(), StringValidator()) | ||||
|         self.assertTrue(validator.isValid(42)) | ||||
|         self.assertTrue(validator.isValid("text")) | ||||
|  | ||||
|     def testRejectsOtherTypes(self): | ||||
|         validator = OrValidator(IntegerValidator(), StringValidator()) | ||||
|         self.assertFalse(validator.isValid(.5)) | ||||
|  | ||||
|     def testRejectsIfNoValidator(self): | ||||
|         validator = OrValidator() | ||||
|         self.assertFalse(validator.isValid("any value")) | ||||
							
								
								
									
										17
									
								
								test/property/validators/test_regex_validator.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								test/property/validators/test_regex_validator.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| from unittest import TestCase | ||||
| from owrx.property.validators import RegexValidator | ||||
| import re | ||||
|  | ||||
|  | ||||
| class RegexValidatorTest(TestCase): | ||||
|     def testMatchesRegex(self): | ||||
|         validator = RegexValidator(re.compile("abc")) | ||||
|         self.assertTrue(validator.isValid("abc")) | ||||
|  | ||||
|     def testDoesntMatchRegex(self): | ||||
|         validator = RegexValidator(re.compile("abc")) | ||||
|         self.assertFalse(validator.isValid("xyz")) | ||||
|  | ||||
|     def testFailsIfValueIsNoString(self): | ||||
|         validator = RegexValidator(re.compile("abc")) | ||||
|         self.assertFalse(validator.isValid(42)) | ||||
							
								
								
									
										14
									
								
								test/property/validators/test_string_validator.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								test/property/validators/test_string_validator.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| from unittest import TestCase | ||||
| from owrx.property.validators import StringValidator | ||||
|  | ||||
| class StringValidatorTest(TestCase): | ||||
|     def testPassesStrings(self): | ||||
|         validator = StringValidator() | ||||
|         self.assertTrue(validator.isValid("text")) | ||||
|  | ||||
|     def testDoesntPassOthers(self): | ||||
|         validator = StringValidator() | ||||
|         self.assertFalse(validator.isValid(123)) | ||||
|         self.assertFalse(validator.isValid(-2)) | ||||
|         self.assertFalse(validator.isValid(.5)) | ||||
|         self.assertFalse(validator.isValid(object())) | ||||
							
								
								
									
										20
									
								
								test/property/validators/test_validator.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								test/property/validators/test_validator.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| from unittest import TestCase | ||||
| from owrx.property.validators import Validator, NumberValidator, LambdaValidator, StringValidator | ||||
|  | ||||
|  | ||||
| class ValidatorTest(TestCase): | ||||
|  | ||||
|     def testReturnsValidator(self): | ||||
|         validator = NumberValidator() | ||||
|         self.assertIs(validator, Validator.of(validator)) | ||||
|  | ||||
|     def testTransformsLambda(self): | ||||
|         def my_callable(v): | ||||
|             return True | ||||
|         validator = Validator.of(my_callable) | ||||
|         self.assertIsInstance(validator, LambdaValidator) | ||||
|         self.assertTrue(validator.isValid("test")) | ||||
|  | ||||
|     def testGetsValidatorByKey(self): | ||||
|         validator = Validator.of("str") | ||||
|         self.assertIsInstance(validator, StringValidator) | ||||
		Reference in New Issue
	
	Block a user
	 Jakob Ketterl
					Jakob Ketterl