Using importlib to run any function on the gateway?

Our app has a few database/calculation heavy functions that we would rather run on our very beefy gateway server than on our clients.

I recently learned about importlib and was playing around with using a Gateway message handler to try to run any arbitrary function via the gateway by giving the payload the function name and arguments for the function. I had two questions about this -

  1. Security wise is this opening any vulnerabilities to me? My form submittal buttons now do a system.util.sendMessage to my abstract gateway message handler feeding the function name and arguments. I think it should be safe but I also never seen anyone do this and I get the feeling it might be for good reason.

  2. How can I actually feed the arguments to the function? Right now I am hardcoding them as such in my gateway handler, which is only set up to handle a few functions

	import importlib
	import system.util
	logger = system.util.getLogger("Logger")
	logger.info("Payload")
	logger.info(str(payload))
	form = payload['form']
	data = payload['data']
	data['user'] = payload['user']
	library = 'forms.'+form
	module = importlib.import_module(library)
	action = payload['action']
	
	if action == 'create':
		module.create(data)

But if I wanted to be able to do it for any arbitrary function with both positional and keyword arguments, how could I feed them to a function?

Not safe. Not terribly easy to attack, but no, not safe.

The overall idea is fine, though. Just preload all your possible code in the gateway. Pass function name, positional parameters, and keyword parameters through your sendRequest() call.

Say you have script module 'beefyFunctions` set up with all the named functions (which can call other script modules as needed). Create a message handler ‘beefy’ like so:

functionName, args, kwargs = [payload[x] for x in ['functionName', 'args', 'kwargs']]
return getattr(beefyFunctions, functionName)(*args, **kwargs)

Then in a client, you’d do this:

payload = {'functionName': 'somePreloadedFunction', 'args': someArgsTuple, 'kwargs': someArgsDictionary}
result = system.util.sendRequest('myProject', 'beefy', payload)
1 Like

Yes, correct me if I’m wrong but it really seems only attackable if someone could do a system.util.sendMessage and they could control all the parameters, which I think in our application would only be possible if they had designer access.

When you say preload all possible code in the gateway, are you saying that my current method where I check for the function name and then directly call it with module.create would be fine? I would just have to add elif's for other functions to call to manually call.

Normally, yes. But a faked or modified client can use the http/https connection to do anything a client is allowed. Faking a client is the "not terribly easy" part.

2 Likes

Just dropping in to concur with Phil, a whitelist is the only secure way to do this. That’s why named queries exist, and the whole ‘client permissions’ aspect was added mid-stream to 7.9; gateways had far too much ‘trust’ of running clients.

2 Likes

By whitelist, is that this part of @pturmel’s script getattr(beefyFunctions, where no matter what the client says, we are only grabbing functions from a specific, hardcoded module?

2 Likes

Yes. You could be even more explicit about it - create a dictionary of function names to functions in the message handler, something like this:

functions = {'history': beefy.history, 'query': beefy.query}

try:
    result = functions[payload['functionName']](*payload['arguments'])
    return result
except KeyError:
    log.errorf("Unauthorized function call to %s attempted", payload['functionName'])
    return "Unauthorized function attempted!"

But getattr should be just as safe - I’m not aware of any way to traverse a hierarchy using getattr. It’s just a choice of maintenance effort vs fully explicit behavior.

1 Like

I get most of this except how functions[payload['functionName']](*payload['arguments']) actually runs the function. I guess prior I would have a import beefy and so it would end up being beefy.history(*payload[‘arguments’]) and thats how it would run?

Yes, exactly, although I don’t think you even need to import beefy, I believe we implicitly load all your project library stuff; don’t quote me on that, though.

1 Like