Merge branch 'fix_arbitrary_code_execution' into develop

This commit is contained in:
Jakob Ketterl 2021-01-24 22:47:08 +01:00
commit a5bdf6c3ac
15 changed files with 389 additions and 17 deletions

View File

@ -4,16 +4,26 @@ from owrx.js8 import Js8Parser
from owrx.aprs import AprsParser from owrx.aprs import AprsParser
from owrx.pocsag import PocsagParser from owrx.pocsag import PocsagParser
from owrx.source import SdrSource, SdrSourceEventClient 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 owrx.modes import Modes
from csdr import csdr from csdr import csdr
import threading import threading
import re
import logging import logging
logger = logging.getLogger(__name__) 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): class DspManager(csdr.output, SdrSourceEventClient):
def __init__(self, handler, sdrSource): def __init__(self, handler, sdrSource):
self.handler = handler self.handler = handler
@ -27,22 +37,42 @@ class DspManager(csdr.output, SdrSourceEventClient):
} }
self.props = PropertyStack() self.props = PropertyStack()
# local demodulator properties not forwarded to the sdr # local demodulator properties not forwarded to the sdr
self.props.addLayer( # ensure strict validation since these can be set from the client
0, # and are used to build executable commands
PropertyLayer().filter( validators = {
"output_rate", "output_rate": "int",
"hd_output_rate", "hd_output_rate": "int",
"squelch_level", "squelch_level": "num",
"secondary_mod", "secondary_mod": ModulationValidator(),
"low_cut", "low_cut": "num",
"high_cut", "high_cut": "num",
"offset_freq", "offset_freq": "int",
"mod", "mod": ModulationValidator(),
"secondary_offset_freq", "secondary_offset_freq": "int",
"dmr_filter", "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 # properties that we inherit from the sdr
self.props.addLayer( self.props.addLayer(
1, 1,
@ -188,7 +218,7 @@ class DspManager(csdr.output, SdrSourceEventClient):
self.setProperty(k, v) self.setProperty(k, v)
def setProperty(self, prop, value): def setProperty(self, prop, value):
self.props[prop] = value self.localProps[prop] = value
def getClientClass(self): def getClientClass(self):
return SdrSource.CLIENT_USER return SdrSource.CLIENT_USER

View File

@ -1,4 +1,5 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from owrx.property.validators import Validator
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -154,6 +155,50 @@ class PropertyFilter(PropertyManager):
return [k for k in self.pm.keys() if k in self.props] 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): class PropertyStack(PropertyManager):
def __init__(self): def __init__(self):
super().__init__() super().__init__()

View 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,
}

View File

@ -48,3 +48,11 @@ class PropertyFilterTest(TestCase):
pf["testkey"] = "new value" pf["testkey"] = "new value"
self.assertEqual(pm["testkey"], "new value") self.assertEqual(pm["testkey"], "new value")
self.assertEqual(pf["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")

View 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"

View File

View 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()))

View 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()))

View 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()))

View 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))

View 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()))

View 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"))

View 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))

View 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()))

View 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)