52 votes

Comment en toute sécurité lire une ligne à partir d'une std::istream?

Je veux en toute sécurité lire une ligne à partir d'un std::istream. Le flux pourrait être n'importe quoi, par exemple, une connexion sur un serveur Web ou quelque chose de traitement des dossiers soumis par des sources inconnues. Il y a beaucoup de réponse de commencer à faire l'équivalent moral de ce code:

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

Compte tenu de la peut-être douteuse source d' in, à l'aide du code ci-dessus conduirait à une vulnérabilité: un agent malveillant peut monter une attaque de déni de service contre ce code à l'aide d'une énorme ligne. Donc, je voudrais limiter la longueur de la ligne d'assez grande valeur, à dire 4 millions chars. Tandis que quelques grandes lignes peuvent être rencontrés, il n'est pas possible d'allouer un tampon pour chaque fichier et utiliser std::istream::getline().

Comment peut-on la taille maximale de la ligne limitée, idéalement sans fausser le code trop mal et sans allouer de gros morceaux de la mémoire avant?

36voto

Rapptz Points 10135

Vous pouvez écrire votre propre version de std::getline avec un nombre maximum de caractères à lire paramètre, quelque chose qui s'appelle getline_n ou quelque chose.

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

Peut-être exagéré.

17voto

Il y a déjà un getline fonction comme une fonction membre d' istream, il vous suffit de l'envelopper de gestion de mémoire tampon.

#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

8voto

nobar Points 5849

Remplacer std::getline par la création d'un wrapper autour de 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; }
    }

Si vous voulez éviter l'excès temporaire des allocations de mémoire, vous pouvez utiliser une boucle qui pousse la répartition selon les besoins (probablement doubler de taille à chaque passage). N'oubliez pas que les exceptions peut ou peut ne pas être activée sur l'objet istream.

Voici une version avec la répartition plus efficace de la stratégie:

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

5voto

Dietmar Kühl Points 70604

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

  1. Écrire une version personnalisée de getline() , éventuellement à l'aide de l' std::istream::getline() membre interne pour obtenir les caractères réels.
  2. L'utilisation d'un filtrage de la mémoire tampon pour limiter la quantité de données potentiellement reçues.
  3. Au lieu de lire un std::string, utilisez une chaîne de l'instanciation d'un allocateur de limiter la quantité de mémoire stockée dans la chaîne.

Toutes les suggestions sont venus avec code. Cette réponse fournit le code pour toutes les approches un peu de la discussion des trois approches. Avant d'entrer dans les détails de mise en œuvre, il est d'abord utile de rappeler qu'il y a plusieurs choix de ce qui se passerait si une trop longue d'entrée est reçu:

  1. La lecture d'un trop long peut entraîner une réussite en lecture partielle de la ligne, c'est à dire, la chaîne résultante contient le lire le contenu et le flux n'a pas tous les indicateurs d'erreur de jeu. Ce faisant, toutefois, qu'il n'est pas possible de faire la distinction entre une ligne de frapper exactement la limite, ou trop long. Car la limite est quelque peu arbitraire de toute façon il n'a probablement pas vraiment d'importance, cependant.
  2. La lecture d'un trop long de la ligne pourrait être considéré comme un échec (c'est à dire, le paramètre std::ios_base::failbit et/ou std::ios_base::bad_bit), et, depuis la lecture a échoué, le rendement d'une chaîne vide. Ce qui donne une chaîne vide, évidemment, empêche potentiellement en regardant la chaîne de lire jusqu'à présent pour peut-être voir ce qu'il se passe.
  3. La lecture d'un trop long de ligne pourrait fournir l'partielle de la ligne de lecture et également définir les indicateurs d'erreur sur le flux. Cela semble raisonnable comportement à la fois de détecter qu'il y a quelque chose, et aussi de fournir les données d'entrée pour le potentiel d'inspection.

Bien qu'il existe de multiples exemples de code, la mise en œuvre d'une version limitée de l' getline() déjà, ici en est une autre! Je pense que c'est plus simple (quoique peut-être plus lente; les performances peuvent être traités, le cas échéant), qui conserve également l' std::getline()s interface: utiliser le flux de l' width() de communiquer une limite (peut-être en prenant width() en compte est une prolongation raisonnable 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;
}

Cette version de l' getline() est utilisé comme std::getline() mais quand il semble raisonnable de limiter la quantité de données à lire, l' width() est définie, par ex.:

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

Une autre approche est d'utiliser un système de filtrage de la mémoire tampon pour limiter le montant de l'apport: le filtre serait tout simplement compter le nombre de caractères traités et limiter la quantité à un nombre suffisant de caractères. Cette approche est en fait plus facile appliqué à un ensemble de flux que d'une ligne individuelle: lors du traitement d'une seule ligne, le filtre ne pouvez pas simplement obtenir des tampons complète de caractères dans le flux sous-jacent, car il n'existe pas de moyen fiable pour mettre les personnages en arrière. La mise en œuvre d'un tampon version est toujours simple, mais probablement pas très efficace:

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

Cette mémoire tampon est déjà configuré pour insérer lui-même lors de la construction et supprimer lui-même à la destruction. Qui est, il peut être utilisé simplement comme ceci:

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

Il serait facile d'ajouter un manipulateur de la configuration de la limite, trop. Un avantage de cette approche est qu'aucun de la lecture de code a besoin d'être touché si la taille totale des flux pourrait être limitée: le filtre pourrait être mis en place juste après la création du flux. Quand il n'est pas nécessaire de retirer le filtre, le filtre peut aussi utiliser un tampon qui pourrait grandement améliorer la performance.

La troisième approche proposée est d'utiliser un std::basic_string avec un allocateur personnalisé. Il y a deux aspects qui sont un peu maladroits dans l'allocateur approche:

  1. La chaîne en cours de lecture, en réalité, a un type qui n'est pas immédiatement convertibles en std::string (même s'il n'est pas difficile de faire la conversion).
  2. La taille maximale de la matrice peut facilement être limité, mais la chaîne va en avoir plus ou moins aléatoire de taille plus petite que: lorsque le flux d'échec de l'allocation d'une exception est levée et il n'y a pas de tentative de pousser la chaîne par une taille plus petite.

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

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

L'allocation serait utilisé quelque chose comme ceci (le code se compile bien avec une version récente de bruit , mais pas avec 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
}

En résumé, il y a plusieurs approche chacune avec son propre petit inconvénient mais chaque raisonnablement viable pour l'objectif de limiter les attaques par déni de service basé sur trop de lignes:

  1. À l'aide d'une version personnalisée de l' getline() signifie que la lecture de code doit être changé.
  2. En utilisant une mesure de la mémoire tampon est lent, sauf si la totalité du flux de la taille peut être limitée.
  3. À l'aide d'un allocateur personnalisé donne moins de contrôle et nécessite quelques modifications à la lecture du code.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X