簡體   English   中英

如何安全地從std :: istream讀取一行?

[英]How to safely read a line from an std::istream?

我想安全地std::istream讀取一行。 流可以是任何東西,例如Web服務器上的連接或處理未知來源提交的文件的東西。 有許多答案開始與該代碼在道德上等效:

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

鑒於in的可能來源可疑,因此使用上述代碼將導致漏洞:惡意代理可能會使用大量代碼對該代碼發起拒絕服務攻擊。 因此,我想將行長度限制為一個相當高的值,例如400萬個char 雖然可能會遇到幾行,但為每個文件分配緩沖區並使用std::istream::getline()並不可行。

如何限制行的最大大小,理想情況下又不會嚴重扭曲代碼,也不會預先分配大塊內存?

您可以編寫自己的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';
}

可能會被矯kill過正。

作為istream的成員函數,已經有一個getline函數,您只需要包裝它即可進行緩沖區管理。

#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 :: 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; }
    }

如果要避免過多的臨時內存分配,可以使用一個循環,該循環根據需要增大分配(每次通過時大小可能加倍)。 不要忘記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. 可能在內部使用std::istream::getline()成員編寫getline()的自定義版本,以獲取實際字符。
  2. 使用過濾流緩沖區來限制可能接收的數據量。
  3. 而不是讀取std::string ,而是使用帶有自定義分配器的字符串實例化來限制存儲在字符串中的內存量。

並非所有建議都隨代碼一起提供。 該答案提供了所有方法的代碼,並對這三種方法進行了一些討論。 在詳細介紹實現細節之前,首先需要指出的是,如果接收到過長的輸入,應該有多種選擇:

  1. 讀取超長行可能會導致成功讀取部分行,即,結果字符串包含讀取的內容,並且流中未設置任何錯誤標志。 但是,這樣做意味着無法區分精確達到極限或太長的直線。 但是,由於該限制在某種程度上是任意的,因此可能實際上並不重要。
  2. 讀取超長行可能被視為失敗(即,設置std::ios_base::failbit和/或std::ios_base::bad_bit ),並且由於讀取失敗而產生空字符串。 顯然,產生一個空字符串會阻止潛在地查看到目前為止所讀取的字符串以了解正在發生的情況。
  3. 讀取超長行可以提供部分行的讀取,還可以在流上設置錯誤標志。 這似乎是合理的行為,既可以檢測到有問題,也可以提供輸入以進行潛在檢查。

盡管已經有多個代碼示例實現了getline()的受限版本, 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與自定義分配器一起使用。 分配器方法有兩個方面有些尷尬:

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

分配器將使用如下形式(代碼使用最新版本的clang編譯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. 使用自定義分配器的控制較少,並且需要對讀取代碼進行一些更改。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM