Подтвердить что ты не робот

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

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

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

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

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

4b9b3361

Ответ 1

Вы можете написать собственную версию 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';
}

Может быть, слишком много.

Ответ 2

Существует уже такая функция 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

Ответ 3

Замените 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;
    }

Ответ 4

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

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

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

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

Хотя существует несколько примеров кода, реализующих ограниченную версию getline(), вот еще один! Я думаю, что это проще (хотя возможно, медленнее, при необходимости можно справляться с производительностью), который также сохраняет интерфейс std::getline(): он использует поток 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 с пользовательским распределителем. Есть два аспекта, которые немного неудобны в отношении подхода распределителя:

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

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

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);
}

Распределитель будет использоваться примерно так (код компилирует ОК с последней версией clang, но не с 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
}

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

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