Уязвимость в парадигме функционального программирования?

Несколько дней назад возникла пара вопросов об уязвимостях переполнения буфера (например, Есть ли у Java переполнение буфера?, Secure C и университеты, обученные переполнению буфера, чтобы назвать пару), которые могут произойти в императивных языках программирования, таких как C.

В функциональном программировании (от очень ограниченной экспозиции, которую я испытывал от Haskell), я вижу, как уязвимости такие как переполнение буфера, не будут возникать, потому что эти проблемы являются результатом изменения состояния программы или области памяти. (Пожалуйста, поправьте меня, если я ошибаюсь.)

Без учета возможности уязвимостей, присутствующих в среде компилятора, интерпретатора или исполнения, существуют ли какие-либо уязвимости безопасности, существующие в парадигме функционального программирования? Существуют ли какие-либо конкретные типы уязвимостей, которые существуют в функциональном программировании, но не в обязательном программировании?

Ответы

Ответ 1

Если программист не ожидает, что [какой-либо ввод] может привести к [потреблению] ресурсов [program], это уязвимость в виде возможной DoS. Это слабость всех Turing-полных языков, которые я видел, но ловкость Haskell затрудняет рассуждение о том, что включает в себя вычисление.

Как пример (надуманный),

import Control.Monad (when)
import System (getArgs)
main = do
    files <- getArgs
    contents <- mapM readFile files
    flip mapM_ (zip files contents) $ \(file, content) ->
        when (null content) $ putStrLn $ file ++ " is empty"

Наивный программист может подумать: "Haskell ленив, поэтому он не откроет и не прочитает файлы, пока это не понадобится", и "Haskell собирает мусор, поэтому, как только это будет сделано с файлом, он может закрыть файл ручка". К сожалению, эта программа фактически просто откроет множество файлов сразу (специфично для реализации), и только пустые файлы будут закрывать свои дескрипторы файлов (побочные эффекты правил жизнеспособности реализации):

$ ghc --make -O2 Test
[1 of 1] Compiling Main             ( Test.hs, Test.o )
Linking Test ...
$ strace -etrace=open,close ./Test dir/* /dev/null
...
open("dir/1", O_RDONLY|O_NOCTTY|O_NONBLOCK|O_LARGEFILE) = 3
open("dir/2", O_RDONLY|O_NOCTTY|O_NONBLOCK|O_LARGEFILE) = 4
open("dir/3", O_RDONLY|O_NOCTTY|O_NONBLOCK|O_LARGEFILE) = 5
open("dir/4", O_RDONLY|O_NOCTTY|O_NONBLOCK|O_LARGEFILE) = 6
open("dir/5", O_RDONLY|O_NOCTTY|O_NONBLOCK|O_LARGEFILE) = 7
...
open("/dev/null", O_RDONLY|O_NOCTTY|O_NONBLOCK|O_LARGEFILE) = 255
close(255)
/dev/null is empty
$

Возможно, вы не ожидаете появления ошибки -EMFILE "Слишком много открытых файлов".

Как я уже сказал, это надуманный пример и может произойти и на других языках, но просто проще пропустить определенные способы использования ресурсов в Haskell.

Ответ 2

Функциональные языки имеют недооцененное преимущество "безопасность через безвестность" из-за их моделей исполнения. Если вы посмотрите на эксплойты безопасности на C-программах, они используют преимущества системы слабых типов, манипуляции с указателями и отсутствие проверки границ, но что еще более важно, они используют хорошо понятную, прямолинейную модель исполнения. Например, вы можете надежно разбить стек на C, потому что относительно легко узнать, где находится стек, просто беря адрес локальных переменных, Многие другие эксплойты полагаются на аналогичное низкоуровневое понимание модели выполнения.

В отличие от этого, не так очевидно, как функциональный код будет скомпилирован до двоичного кода, поэтому не так просто разработать рецепт для выполнения введенного кода или доступа к привилегированным данным. Как ни странно, неизвестность моделей исполнения обычно считается слабостью функциональных языков, поскольку программисты не всегда имеют хорошую интуицию в отношении того, как их код будет работать.

Ответ 3

Я так не думаю.

Как я вижу, вместо парадигмы программирования уязвимости, такие как переполнение буфера, имеют больше общего с архитектурой/виртуальной машиной компилятора/интерпретатора.

Например, если вы используете функциональное программирование в среде .NET(С#, VB и т.д.), до тех пор, пока вы работаете с управляемыми объектами, будьте осторожны при переполнении буфера.

Ответ 4

Возможно, будет проще вызывать неограниченную рекурсию (и результирующее использование памяти) в функциональной реализации, чем в соответствующей императивной реализации. В то время как настоятельная программа может просто перейти в бесконечный цикл при обработке некорректных входных данных, функциональная программа может вместо этого собирать всю память, которую она может при выполнении той же обработки. Как правило, это будет ответственность VM, в которой программа работает, чтобы ограничить объем памяти, которую может потреблять определенный процесс. (Даже для собственных процессов Unix, ulimit может обеспечить эту защиту.)