Как безопасно прочитать строку из std:: istream?


Я хочу безопасное читать строку с std::istream. Поток может быть чем угодно, например, соединение на веб-сервере или что-то, обрабатывающее файлы, представленные неизвестными источниками. Есть много ответов, начиная делать моральный эквивалент этого кода:

void read(std::istream& in) {
    std::string line;
    if (std::getline(in, line)) {
        // process the line
    }
}

учитывая, возможно, сомнительный источник in, использование вышеуказанного кода приведет к уязвимости: вредоносный агент может смонтировать атаку отказа в обслуживании против этого кода, используя огромную строку. Таким Образом, Я хотелось бы ограничить длину линии до некоторого довольно высокого значения, скажем 4 миллионов chars. хотя можно встретить несколько больших строк, нецелесообразно выделять буфер для каждого файла и использовать std::istream::getline().

как можно ограничить максимальный размер строки, в идеале не искажая код слишком сильно и не выделяя большие куски памяти спереди?

4   51  

4 ответа:

вы можете написать свою собственную версию std::getline с параметром чтения максимального количества символов, что-то называется getline_n или что-то.

#include <string>
#include <iostream>

template<typename CharT, typename Traits, typename Alloc>
auto getline_n(std::basic_istream<CharT, Traits>& in, std::basic_string<CharT, Traits, Alloc>& str, std::streamsize n) -> decltype(in) {
    std::ios_base::iostate state = std::ios_base::goodbit;
    bool extracted = false;
    const typename std::basic_istream<CharT, Traits>::sentry s(in, true);
    if(s) {
        try {
            str.erase();
            typename Traits::int_type ch = in.rdbuf()->sgetc();
            for(; ; ch = in.rdbuf()->snextc()) {
                if(Traits::eq_int_type(ch, Traits::eof())) {
                    // eof spotted, quit
                    state |= std::ios_base::eofbit;
                    break;
                }
                else if(str.size() == n) {
                    // maximum number of characters met, quit
                    extracted = true;
                    in.rdbuf()->sbumpc();
                    break;
                }
                else if(str.max_size() <= str.size()) {
                    // string too big
                    state |= std::ios_base::failbit;
                    break;
                }
                else {
                    // character valid
                    str += Traits::to_char_type(ch);
                    extracted = true;
                }
            }
        }
        catch(...) {
            in.setstate(std::ios_base::badbit);
        }
    }

    if(!extracted) {
        state |= std::ios_base::failbit;
    }

    in.setstate(state);
    return in;
}

int main() {
    std::string s;
    getline_n(std::cin, s, 10); // maximum of 10 characters
    std::cout << s << '\n';
}

может быть перебор.

уже есть такая getline функция как функция-член istream, вам просто нужно обернуть его для управления буфером.

#include <assert.h>
#include <istream>
#include <stddef.h>         // ptrdiff_t
#include <string>           // std::string, std::char_traits

typedef ptrdiff_t Size;

namespace my {
    using std::istream;
    using std::string;
    using std::char_traits;

    istream& getline(
        istream& stream, string& s, Size const buf_size, char const delimiter = '\n'
        )
    {
        s.resize( buf_size );  assert( s.size() > 1 );
        stream.getline( &s[0], buf_size, delimiter );
        if( !stream.fail() )
        {
            Size const n = char_traits<char>::length( &s[0] );
            s.resize( n );      // Downsizing.
        }
        return stream;
    }
}  // namespace my

заменить std:: getline создать обертку вокруг std:: istream:: getline:

std::istream& my::getline( std::istream& is, std::streamsize n, std::string& str, char delim )
    {
    try
       {
       str.resize(n);
       is.getline(&str[0],n,delim);
       str.resize(is.gcount());
       return is;
       }
    catch(...) { str.resize(0); throw; }
    }

Если вы хотите избежать чрезмерного выделения временной памяти, вы можете использовать цикл, который увеличивает выделение по мере необходимости (вероятно, удвоение размера на каждом проходе). Не забывайте, что исключения могут быть включены или не включены для объекта istream.

вот версия с более эффективной стратегии распределения:

std::istream& my::getline( std::istream& is, std::streamsize n, std::string& str, char delim )
    {
    std::streamsize base=0;
    do {
       try
          {
          is.clear();
          std::streamsize chunk=std::min(n-base,std::max(static_cast<std::streamsize>(2),base));
          if ( chunk == 0 ) break;
          str.resize(base+chunk);
          is.getline(&str[base],chunk,delim);
          }
       catch( std::ios_base::failure ) { if ( !is.gcount () ) str.resize(0), throw; }
       base += is.gcount();
       } while ( is.fail() && is.gcount() );
    str.resize(base);
    return is;
    }

на основе комментариев и ответов, кажется, есть три подхода:

  1. написать пользовательскую версию getline() возможно, с помощью std::istream::getline() член внутренне, чтобы получить фактические символы.
  2. используйте буфер потока фильтрации для ограничения количества потенциально полученных данных.
  3. вместо того, чтобы читать std::string, используйте экземпляр строки с пользовательским распределителем, ограничивающим объем памяти, хранящейся в памяти строка.

не все предложения, пришел с кодом. Этот ответ содержит код для всех подходов и немного обсуждения всех трех подходов. Прежде чем перейти к деталям реализации, сначала стоит отметить, что существует несколько вариантов того, что должно произойти, если получен чрезмерно длинный ввод:

  1. чтение слишком длинной строки может привести к успешному чтению частичной строки, т. е. результирующая строка содержит чтение содержимое и поток не имеют никаких флагов ошибок. Однако это означает, что невозможно отличить линию, достигающую точно предела или являющуюся слишком длинной. Поскольку предел несколько произволен в любом случае, это, вероятно, не имеет большого значения.
  2. чтение слишком длинной строки может считаться ошибкой (т. е. установка std::ios_base::failbit и/или std::ios_base::bad_bit) и, поскольку чтение не удалось, дать пустую строку. Получение пустой строки, очевидно, предотвращает потенциальный поиск в строке читать до сих пор, чтобы, возможно, увидеть, что происходит.
  3. чтение слишком длинной строки может обеспечить частичное чтение строки, а также установить флаги ошибок в потоке. Это кажется разумным поведением, как обнаруживающим, что что-то есть, так и обеспечивающим вход для потенциальной проверки.

хотя есть несколько примеров кода, реализующих ограниченную версию getline() уже, вот еще один! Я думаю, что это проще (хотя, возможно, медленнее; производительность может быть решена при необходимости) , который также сохраняет std::getline()интерфейс s: он использует поток width() чтобы сообщить предел (возможно, принимая width() в расчет принимается разумное расширение до std::getline()):

template <typename cT, typename Traits, typename Alloc>
std::basic_istream<cT, Traits>&
safe_getline(std::basic_istream<cT, Traits>& in,
             std::basic_string<cT, Traits, Alloc>& value,
             cT delim)
{
    typedef std::basic_string<cT, Traits, Alloc> string_type;
    typedef typename string_type::size_type size_type;

    typename std::basic_istream<cT, Traits>::sentry cerberos(in);
    if (cerberos) {
        value.clear();
        size_type width(in.width(0));
        if (width == 0) {
            width = std::numeric_limits<size_type>::max();
        }
        std::istreambuf_iterator<char> it(in), end;
        for (; value.size() != width && it != end; ++it) {
            if (!Traits::eq(delim, *it)) {
                value.push_back(*it);
            }
            else {
                ++it;
                break;
            }
        }
        if (value.size() == width) {
            in.setstate(std::ios_base::failbit);
        }
    }
    return in;
}

эта версия getline() используется так же, как std::getline() но когда кажется разумным ограничить объем считываемых данных, то width() установлен, например:

std::string line;
if (safe_getline(in >> std::setw(max_characters), line)) {
    // do something with the input
}

другой подход заключается в том, чтобы просто использовать буфер потока фильтрации для ограничения количество входных данных: фильтр просто подсчитает количество обработанных символов и ограничит количество подходящим количеством символов. Этот подход на самом деле проще применить ко всему потоку, чем к отдельной строке: при обработке только одной строки фильтр не может просто получить буферы, полные символов из базового потока, потому что нет надежного способа вернуть символы. Реализация небуферизованной версии по-прежнему проста, но, вероятно, не особенно эффективный:

template <typename cT, typename Traits = std::char_traits<char> >
class basic_limitbuf
    : std::basic_streambuf <cT, Traits> {
public:
    typedef Traits                    traits_type;
    typedef typename Traits::int_type int_type;

private:
    std::streamsize                   size;
    std::streamsize                   max;
    std::basic_istream<cT, Traits>*   stream;
    std::basic_streambuf<cT, Traits>* sbuf;

    int_type underflow() {
        if (this->size < this->max) {
            return this->sbuf->sgetc();
        }
        else {
            this->stream->setstate(std::ios_base::failbit);
            return traits_type::eof();
        }
    }
    int_type uflow()     {
        if (this->size < this->max) {
            ++this->size;
            return this->sbuf->sbumpc();
        }
        else {
            this->stream->setstate(std::ios_base::failbit);
            return traits_type::eof();
        }
    }
public:
    basic_limitbuf(std::streamsize max,
                   std::basic_istream<cT, Traits>& stream)
        : size()
        , max(max)
        , stream(&stream)
        , sbuf(this->stream->rdbuf(this)) {
    }
    ~basic_limitbuf() {
        std::ios_base::iostate state = this->stream->rdstate();
        this->stream->rdbuf(this->sbuf);
        this->stream->setstate(state);
    }
};

этот буфер потока уже настроен для вставки себя при построении и удаления себя при разрушении. То есть, его можно использовать просто так:

std::string line;
basic_limitbuf<char> sbuf(max_characters, in);
if (std::getline(in, line)) {
    // do something with the input
}

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

третий предлагаемый подход заключается в использовании std::basic_string с пользовательским распределителем. Есть два аспекта, которые немного неудобны в отношении подхода распределителя:

  1. считываемая строка на самом деле имеет тип, который не сразу преобразуется в std::string (хотя это также не трудно сделать преобразование).
  2. максимальный размер массива может быть легко ограничено, но строка будет иметь некоторый более или менее случайный размер меньше этого: когда поток не выделяет исключение, возникает исключение, и нет попытки увеличить строку на меньший размер.

вот необходимый код для распределителя, ограничивающего выделенный размер:

template <typename T>
struct limit_alloc
{
private:
    std::size_t max_;
public:
    typedef T value_type;
    limit_alloc(std::size_t max): max_(max) {}
    template <typename S>
    limit_alloc(limit_alloc<S> const& other): max_(other.max()) {}
    std::size_t max() const { return this->max_; }
    T* allocate(std::size_t size) {
        return size <= max_
            ? static_cast<T*>(operator new[](size))
            : throw std::bad_alloc();
    }
    void  deallocate(void* ptr, std::size_t) {
        return operator delete[](ptr);
    }
};

template <typename T0, typename T1>
bool operator== (limit_alloc<T0> const& a0, limit_alloc<T1> const& a1) {
    return a0.max() == a1.max();
}
template <typename T0, typename T1>
bool operator!= (limit_alloc<T0> const& a0, limit_alloc<T1> const& a1) {
    return !(a0 == a1);
}

распределитель будет использоваться примерно так (код компилируется OK с последней версией лязгом но не с gcc):

std::basic_string<char, std::char_traits<char>, limit_alloc<char> >
    tmp(limit_alloc<char>(max_chars));
if (std::getline(in, tmp)) {
    std::string(tmp.begin(), tmp.end());
    // do something with the input
}

таким образом, существует несколько подходов, каждый из которых имеет свой небольшой недостаток, но каждый из них достаточно жизнеспособен для заявленной цели ограничения атак типа "отказ в обслуживании" на основе слишком длинных строк:

  1. использование пользовательской версии getline() означает, что код чтения должен быть изменен.
  2. использование пользовательского буфера потока выполняется медленно, если размер всего потока не может быть ограничен.
  3. использование пользовательского распределителя дает меньше контроля и требует некоторых изменений в чтении кода.