如何从std :: istream中安全地读取一行?

Die*_*ühl 52 c++

我想安全地读一行std::istream.流可以是任何东西,例如,Web服务器上的连接或处理由未知来源提交的文件的东西.有许多答案开始做这个代码的道德等价物:

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

鉴于可能的可疑来源in,使用上述代码会导致漏洞:恶意代理可能会使用巨大的线路对此代码发起拒绝服务攻击.因此,我想将线路长度限制在一个相当高的值,比如4百万char秒.虽然可能会遇到一些大行,但为每个文件分配缓冲区并使用是不可行的std::istream::getline().

如何限制线路的最大尺寸,理想情况下不会过于严重地扭曲代码并且不预先分配大块内存?

Rap*_*ptz 36

您可以编写自己的版本,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';
}
Run Code Online (Sandbox Code Playgroud)

可能会有点矫枉过正.

  • 编写`getline()`的版本可能是一个选项(特别是因为我过去实际上已经实现了所有的IOStreams库).我不知道为什么它没有发生在我身上:也许我太专注于其他两个解决方案(到目前为止只提到了其中一个). (3认同)

Che*_*Alf 17

已经存在这样的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
Run Code Online (Sandbox Code Playgroud)

  • @Inverse:没有什么可以让人感到不安.你可能会对分裂感到不安,因为它可能会被归为零.在这段代码中,值的相关约束(字符串长度必须> 0)由`assert`表示,这通常是一种很好的做法,使得代码比没有它更安全.使用`assert`必须努力才能生成UB的鼻守护进程,即,使用无效参数调用函数`buf_size`*和*执行此操作并定义`NDEBUG`以便抑制`assert` .这就是你应该使用`assert`的原因. (4认同)

nob*_*bar 8

通过在std :: istream :: getline周围创建一个包装来替换std :: 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; }
    }
Run Code Online (Sandbox Code Playgroud)

如果你想避免过多的临时内存分配,你可以使用一个循环来根据需要增加分配(每次传递可能会增加一倍).不要忘记在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;
    }
Run Code Online (Sandbox Code Playgroud)


Die*_*ühl 5

根据评论和答案,似乎有三种方法:

  1. 编写getline()可能在std::istream::getline()内部使用成员的自定义版本以获取实际字符.
  2. 使用过滤流缓冲区来限制可能收到的数据量.
  3. 而不是读取std::string,使用字符串实例化与自定义分配器限制存储在字符串中的内存量.

并非所有建议都附带代码.这个答案为所有方法提供了代码,并对所有三种方法进行了一些讨论.在进入实施细节之前,首先要指出的是,如果收到过长的输入,会有多种选择:

  1. 读取超长行可能导致成功读取部分行,即结果字符串包含读取内容,并且流没有设置任何错误标志.但是,这样做意味着无法区分确切地达到极限或太长的线.但是,由于限制在某种程度上是任意的,所以它可能并不重要.
  2. 读取超长行可能被视为失败(即设置std::ios_base::failbit和/或std::ios_base::bad_bit),并且由于读取失败,因此产生空字符串.显然,产生一个空字符串会阻止潜在地查看到目前为止读取的字符串,以便可能看到正在发生的事情.
  3. 读取超长行可以提供部分行读取并在流上设置错误标志.这似乎是合理的行为,既检测到有什么东西,也提供潜在检查的输入.

虽然有多个代码示例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;
}
Run Code Online (Sandbox Code Playgroud)

这个版本的getline()使用就像std::getline()但是当限制读取的数据量似乎合理时,width()设置,例如:

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

另一种方法是仅使用过滤流缓冲区来限制输入量:过滤器只计算处理的字符数,并将数量限制为适当的字符数.这种方法实际上比单个行更容易应用于整个流:当只处理一行时,过滤器不能只从底层流中获取充满字符的缓冲区,因为没有可靠的方法来放回字符.实现无缓冲版本仍然很简单,但可能效率不高:

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);
    }
};
Run Code Online (Sandbox Code Playgroud)

该流缓冲器已经设置为在构造时插入自身并在破坏时自行移除.也就是说,它可以像这样使用:

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

添加操纵器设置限制也很容易.这种方法的一个优点是,如果流的总大小可能受到限制,则不需要触摸任何读取代码:可以在创建流之后立即设置过滤器.当不需要退出过滤器时,过滤器也可以使用缓冲器,这将大大提高性能.

建议的第三种方法是使用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);
}
Run Code Online (Sandbox Code Playgroud)

分配器将使用类似这样的东西(代码使用最新版本的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
}
Run Code Online (Sandbox Code Playgroud)

总之,有多种方法,每种方法都有其自身的小缺点,但每种方法都适用于基于超长线限制拒绝服务攻击的既定目标:

  1. 使用自定义版本getline()意味着需要更改阅读代码.
  2. 除非可以限制整个流的大小,否则使用自定义流缓冲区很慢.
  3. 使用自定义分配器可以减少控制,并且需要对读取代码进行一些更改.