Comment lire en toute sécurité une ligne d’un std :: istream?

Je veux lire en toute sécurité une ligne d’un std::istream . Le stream peut être n’importe quoi, par exemple une connexion sur un serveur Web ou quelque chose traitant des fichiers soumis par des sources inconnues. Il y a beaucoup de réponses qui commencent à faire l’équivalent moral de ce code:

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

Étant donné la source de données potentiellement douteuse, l’utilisation du code ci-dessus entraînerait une vulnérabilité: un agent malveillant pourrait lancer une attaque par déni de service contre ce code en utilisant une ligne énorme. Ainsi, je voudrais limiter la longueur de la ligne à une valeur plutôt élevée, disons 4 millions de caractères. Bien que quelques lignes importantes puissent être rencontrées, il n’est pas viable d’allouer un tampon pour chaque fichier et d’utiliser std::istream::getline() .

Comment limiter la taille maximale de la ligne, idéalement sans trop dénaturer le code et sans allouer de gros morceaux de mémoire au départ?

Vous pouvez écrire votre propre version de std::getline avec un nombre maximum de caractères en lecture, quelque chose appelé getline_n ou quelque chose.

 #include  #include  template auto getline_n(std::basic_istream& in, std::basic_ssortingng& str, std::streamsize n) -> decltype(in) { std::ios_base::iostate state = std::ios_base::goodbit; bool extracted = false; const typename std::basic_istream::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'; } 

Peut-être trop exagéré.

Il existe déjà une fonction getline tant que fonction membre de istream , il vous suffit de l’envelopper pour la gestion des tampons.

 #include  #include  #include  // ptrdiff_t #include  // std::ssortingng, std::char_traits typedef ptrdiff_t Size; namespace my { using std::istream; using std::ssortingng; using std::char_traits; istream& getline( istream& stream, ssortingng& 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::length( &s[0] ); s.resize( n ); // Downsizing. } return stream; } } // namespace my 

Remplacez std :: getline en créant un wrapper autour de std :: istream :: getline :

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

Si vous souhaitez éviter des allocations de mémoire temporaires excessives, vous pouvez utiliser une boucle qui augmente l’allocation selon vos besoins (dont la taille doublera probablement à chaque passage). N’oubliez pas que les exceptions peuvent ou non être activées sur l’object istream.

Voici une version avec la stratégie d’allocation la plus efficace:

 std::istream& my::getline( std::istream& is, std::streamsize n, std::ssortingng& str, char delim ) { std::streamsize base=0; do { try { is.clear(); std::streamsize chunk=std::min(n-base,std::max(static_cast(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; } 

Sur la base des commentaires et des réponses, il semble y avoir trois approches:

  1. Écrivez une version personnalisée de getline() en utilisant éventuellement le membre std::istream::getline() interne pour obtenir les caractères réels.
  2. Utilisez un tampon de stream de filtrage pour limiter la quantité de données potentiellement reçue.
  3. Au lieu de lire un std::ssortingng , utilisez une instanciation de chaîne avec un allocateur personnalisé limitant la quantité de mémoire stockée dans la chaîne.

Toutes les suggestions ne sont pas accompagnées de code. Cette réponse fournit un code pour toutes les approches et un peu de discussion sur les trois approches. Avant d’entrer dans les détails de la mise en œuvre, il est important de souligner qu’il existe de multiples choix de ce qui devrait arriver si un apport excessivement long est reçu:

  1. La lecture d’une ligne trop longue peut entraîner une lecture réussie d’une ligne partielle, c’est-à-dire que la chaîne résultante contient le contenu lu et que le stream ne contient aucun indicateur d’erreur. Cela signifie cependant qu’il n’est pas possible de faire la distinction entre une ligne qui atteint exactement la limite ou une ligne trop longue. Comme la limite est quelque peu arbitraire de toute façon, cela n’a probablement pas d’importance.
  2. La lecture d’une ligne trop longue peut être considérée comme un échec (par exemple, définir std::ios_base::failbit et / ou std::ios_base::bad_bit ) et, comme la lecture a échoué, générer une chaîne vide. Si vous cédez une chaîne vide, évitez de regarder la chaîne lue jusqu’à présent pour voir ce qui se passe.
  3. La lecture d’une ligne trop longue pourrait fournir la lecture de ligne partielle et définir des indicateurs d’erreur sur le stream. Cela semble être un comportement raisonnable, à la fois en détectant quelque chose et en fournissant des informations pour une inspection potentielle.

Bien qu’il existe déjà plusieurs exemples de code implémentant une version limitée de getline() , en voici une autre! Je pense que c’est plus simple (mais peut-être plus lent; les performances peuvent être traitées si nécessaire), ce qui conserve également l’interface std::getline() : il utilise la width() du stream width() pour communiquer une limite (prendre en compte width() une extension raisonnable de std::getline() ):

 template  std::basic_istream& safe_getline(std::basic_istream& in, std::basic_ssortingng& value, cT delim) { typedef std::basic_ssortingng ssortingng_type; typedef typename ssortingng_type::size_type size_type; typename std::basic_istream::sentry cerberos(in); if (cerberos) { value.clear(); size_type width(in.width(0)); if (width == 0) { width = std::numeric_limits::max(); } std::istreambuf_iterator 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; } 

Cette version de getline() est utilisée comme std::getline() mais quand il semble raisonnable de limiter la quantité de données lue, la width() est définie, par exemple:

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

Une autre approche consiste à utiliser un tampon de stream de filtrage pour limiter la quantité d’entrée: le filtre comptera simplement le nombre de caractères traités et limitera la quantité à un nombre approprié de caractères. Cette approche est en fait plus facile à appliquer à un stream entier qu’à une ligne individuelle: lors du traitement d’une seule ligne, le filtre ne peut pas simplement obtenir des tampons pleins de caractères du stream sous-jacent, car il n’existe aucun moyen fiable de remettre les caractères. L’implémentation d’une version non tamponnée rest simple mais probablement pas particulièrement efficace:

 template  > class basic_limitbuf : std::basic_streambuf  { public: typedef Traits traits_type; typedef typename Traits::int_type int_type; private: std::streamsize size; std::streamsize max; std::basic_istream* stream; std::basic_streambuf* 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& 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); } }; 

Ce tampon de stream est déjà configuré pour s’insérer lors de la construction et se retirer lors de la destruction. Autrement dit, il peut être utilisé simplement comme ceci:

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

Il serait également facile d’append un manipulateur définissant la limite. Un avantage de cette approche est qu’aucun code de lecture ne doit être touché si la taille totale du stream peut être limitée: le filtre peut être configuré juste après la création du stream. Lorsque le filtre n’est pas nécessaire, le filtre peut également utiliser un tampon qui améliore considérablement les performances.

La troisième approche proposée consiste à utiliser un std::basic_ssortingng avec un allocateur personnalisé. L’approche allocateur comporte deux aspects:

  1. La chaîne en cours de lecture a en fait un type qui n’est pas immédiatement convertible en std::ssortingng (bien qu’il ne soit pas difficile de faire la conversion).
  2. La taille maximale du tableau peut être facilement limitée, mais la chaîne aura une taille plus ou moins aléatoire: lorsque le stream échoue, une exception est levée et il n’y a pas de tentative de croissance de la chaîne.

Voici le code nécessaire pour un allocateur limitant la taille allouée:

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

L’allocateur serait utilisé quelque chose comme ceci (le code comstack OK avec une version récente de clang mais pas avec gcc ):

 std::basic_ssortingng, limit_alloc > tmp(limit_alloc(max_chars)); if (std::getline(in, tmp)) { std::ssortingng(tmp.begin(), tmp.end()); // do something with the input } 

En résumé, il existe des approches multiples, chacune avec son petit inconvénient, mais chacune étant raisonnablement viable dans le but déclaré de limiter les attaques par déni de service basées sur des lignes trop longues:

  1. L’utilisation d’une version personnalisée de getline() signifie que le code de lecture doit être modifié.
  2. L’utilisation d’un tampon de stream personnalisé est lente, sauf si la taille du stream entier peut être limitée.
  3. L’utilisation d’un allocateur personnalisé donne moins de contrôle et nécessite certaines modifications du code de lecture.