0

When hardening a system you want to remove any features that you do not need. I have been thinking about this concept from the perspective of interpreters like Python, NodeJS, PHP etc. and am wondering if it could provide any meaningful benefits to analyze what features of the interpreter an application uses and exclude all other features to limit an application's attack surface.

For example, if a PHP application doesn't use system(), it would be preferable to exclude all support for it when deploying the finished application. From my understanding this could have a positive influence on the security of the application since a potential LFI or code injection would not be able to use system() to execute arbitrary commands on the host. PHP in particular has support for this in php.ini, where specific functions can be disabled but I have not found anything similar for Python or NodeJS. I can think of several reasons as to why this could be but would like a second opinion.

  • If real-world applications generally use all features (with a notable impact on security/performance) of their interpreter, being able to disable specific features would not be useful.
  • Considering the large number of dependencies in e.g. a Node project it seems likely that some dependency would always need a specific feature, finding which features are unused across all dependences could thus require a lot of work without providing a sufficient upside to motivate it in the first place.
  • Excluding a built-in function in the same manner as in PHP could cause unnecessarily complicated consequences and create stability issues in the interpreter.
  • Security measures would be better allocated at preventing code injections wholesale rather than limiting what an attacker can do when one is presented.

On Gentoo systems, where every package is built from source, there are USE flags which are somewhat related to this question. USE flags allow for specific features to be enabled/disabled in a package before compiling it. For example, Python can be compiled without support for sqlite if it is not needed. However, I suspect that compiling a tailor-made interpreter for a program from source would generally be done for compatibility reasons or to save space rather than for security reasons.

I am essentially asking if it could be a beneficial trade-off to allow for more granular hardening of Python or NodeJS and/or which interfaces (besides USE-flags) currently exist to limit features in these interpreters?

1
  • Instead of relying on disabling features in the programming language it might be better to put the application inside a sandboxed environment, i.e. container, jail, chroot, VM ... . This way one can both more easy and more reliable restrict what harm the application actually might do. Nov 3, 2021 at 7:47

1 Answer 1

0

Considering the large number of dependencies in e.g. a Node project it seems likely that some dependency would always need a specific feature, finding which features are unused across all dependences could thus require a lot of work without providing a sufficient upside to motivate it in the first place.

This is probably the best reason why the idea would be hard to make work. It's especially bad because, for non-trivial JS codebases, it may literally be impossible to programmatically determine all the APIs that might ever be used (consider globalThis[strVar], which can be literally anything on the global object, limited only by the possible values of strVar, or for that matter any use of eval or eval-equivalents).

Security measures would be better allocated at preventing code injections wholesale rather than limiting what an attacker can do when one is presented.

This is one reason why it's not a priority. Breaking likely payloads is helpful, but the vast majority of deployments have much lower hanging fruit in terms of security improvements. Code hardening, sandboxing, and attack surface reduction (e.g. minimal listeners or external requests, no unneeded services, etc.) are all generally going to pay off better.

Another thing you didn't mention is that in a lot of cases, the stuff you're talking about isn't even meaningfully removing attacker options. For example, suppose you disallowed child_process.exec and child_process.exec_sync but not child_process.spawn; that might break a very brittle payload that assumes everything is there, but it won't break a slightly more clever one. Even if you block the entire child_process module, there are lots of other ways an attacker could launch another process, such as writing to a file that will be executed (such as a .profile or a crontab) or by downloading a Node module that contains a native component that launches processes (via execve / CreateProcessW) and then requireing it. Since Node and Python both make it very easy to wrap native code, and to load additional code at runtime, you would have to severely cripple the runtime to prevent an attacker who achieved inside-the-runtime code injection from breaking out of it.

Finally, as implied above, sandboxing can solve this problem better anyhow. Instead of removing file APIs, just make most of the file system inaccessible. Instead of removing subprocess APIs, just drop the kernel capability to spawn new processes, or at least run in such a limited-privilege container that they can't do anything interesting anyhow. As a bonus, this works even if the attacker exploits an actual memory corruption vuln (rather than just code injection) and gains control of the runtime itself; it does no good to remove the child_process module if the attacker can directly call execve by function lookup or direct system call invocation, but OS-level or hypervisor-level sandboxing can make doing so fruitless.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .