¿Cómo leer con seguridad una línea de un std::istream?

15 minutos de lectura

avatar de usuario
Dietmar Kühl

Yo quiero sin peligro leer una línea de un std::istream. La secuencia puede ser cualquier cosa, por ejemplo, una conexión en un servidor web o algo que procesa archivos enviados por fuentes desconocidas. Hay muchas respuestas que comienzan a hacer el equivalente moral de este código:

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

Dada la fuente posiblemente dudosa de in, el uso del código anterior daría lugar a una vulnerabilidad: un agente malicioso puede realizar un ataque de denegación de servicio contra este código utilizando una línea enorme. Por lo tanto, me gustaría limitar la longitud de la línea a un valor bastante alto, digamos 4 millones. chars. Si bien se pueden encontrar algunas líneas grandes, no es viable asignar un búfer para cada archivo y uso. std::istream::getline().

¿Cómo se puede limitar el tamaño máximo de la línea, idealmente sin distorsionar demasiado el código y sin asignar grandes cantidades de memoria por adelantado?

  • ¿Qué pasa con un asignador personalizado que arrojará si se le pide que asigne más allá de su umbral? construir un basic_string objeto usando ese asignador y leer en él.

    – Pretoriano

    17 dic 2013 a las 22:00

  • Tal vez subclasificando std::string y proporcionando un max_size() función que escupe algo pequeño?

    – Colin

    17 de diciembre de 2013 a las 22:07


  • @Praetorian: Supongo que usar un asignador sería una opción. Lamentablemente, cambia el tipo de std::string.

    – Dietmar Kühl

    17 de diciembre de 2013 a las 22:09

  • Podrías reemplazar el streambuf de in con su propia implementación que envuelve el streambuf original y envía un '\n' cuando se lee cierto número de caracteres.

    – jrok

    17 de diciembre de 2013 a las 22:39


  • @DietmarKühl: tal vez pueda intentar simplemente verificar la cantidad de caracteres en el búfer antes de extraer: if (in.rdbuf()->in_avail() > max_size) { /* end */ }

    – mb84

    19 de diciembre de 2013 a las 18:40

avatar de usuario
Rapptz

Podrías escribir tu propia versión de std::getline con un número máximo de caracteres leer parámetro, algo llamado getline_n o algo.

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

Sin embargo, podría ser excesivo.

  • Escribir una versión de getline() podría ser una opción (especialmente porque lo he hecho en el pasado al implementar toda la biblioteca IOStreams). No sé por qué no se me ocurrió: tal vez estaba demasiado concentrado en otras dos soluciones (solo una de las cuales se mencionó hasta ahora).

    – Dietmar Kühl

    17 de diciembre de 2013 a las 22:21

  • +1. Lo único que cuestiono es la llamada a reserve, ya que el OP está hablando del orden de 4 Mbytes como guardia, pero es posible que solo esté tratando con cadenas de un tamaño mucho más pequeño. Puede ser mejor que el usuario realice la reserva por su cuenta.

    – David S.

    17 de diciembre de 2013 a las 22:25


  • @DaveS Bastante gracioso, la versión original que escribí no tenía el reserve llame pero lo agregué en buena medida. Si fuera por mí, tampoco lo habría hecho. Me imagino que simplemente lo quitaré.

    – Rapptz

    17 de diciembre de 2013 a las 22:26


  • Estoy confundido: ¿dónde verifica este código el carácter de nueva línea?

    – Siler

    18/09/2018 a las 17:41

Ya existe tal getline funcionan como una función miembro de istreamsolo necesita envolverlo para la gestión del búfer.

#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

  • &s[0] me hace sentir incómodo

    – Inverso

    20 de diciembre de 2013 a las 0:16

  • @Inverse: No hay nada por lo que sentirse incómodo. También podría sentirse incómodo con una división, razonando que podría resultar ser una división por cero. En este código, la restricción relevante sobre el valor (que la longitud de la cadena debe ser >0) se expresa mediante un assert, que generalmente es una buena práctica y hace que el código sea más seguro que sin él. Con el assert uno debe trabajar duro para generar los demonios nasales de UB, es decir, llamar a la función con un argumento inválido para buf_size y haz esto con NDEBUG definida para suprimir la assert. Por eso debes usar assert.

    – Saludos y hth. – alf

    20 de diciembre de 2013 a las 6:53


  • Oh, quise decir eso, mi entendimiento es que los datos en un std::string no se garantiza que sea continuo, es solo .c_str() eso es. Asi que &v[0] está bien para un std::vector pero no estrictamente para std::string

    – Inversa

    17 de enero de 2014 a las 15:54

  • @Inverso: std::string el búfer se ha garantizado contiguo desde la reunión de Lillehammer en 2005. Por supuesto, no fue oficial hasta C++ 11, pero ningún proveedor de compiladores podría vender un compilador donde introdujeron lo contrario, dadas las dependencias de código existentes y la redacción conocida. en el futuro estándar C++11. En ese momento, una forma de identificar a los abogados de lenguaje ficticio era discutir si se podía confiar en esto. 😉

    – Saludos y hth. – alf

    18 de enero de 2014 a las 2:42

  • ¿La copia en escritura std::string es un problema? (Ciertamente hubo implementaciones de este tipo a principios de la década de 2000, aunque C++ 11 las prohíbe)

    –MM

    18 de diciembre de 2014 a las 3:23


avatar de usuario
brent bradburn

Reemplazar std::getline creando un envoltorio alrededor estándar::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 desea evitar asignaciones de memoria temporal excesivas, puede usar un ciclo que aumenta la asignación según sea necesario (probablemente duplicando el tamaño en cada paso). No olvide que las excepciones pueden o no estar habilitadas en el objeto istream.

Aquí hay una versión con la estrategia de asignación más eficiente:

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

  • Como se implementó, ambos dejarán un carácter de terminación ‘\0’ en la cadena generada. Esto no es normal para una cadena C++, por lo que una mejora sería sacar el carácter final antes de regresar. Tenga en cuenta que el cambio de tamaño basado en escanear la cadena en busca de ‘\0’ puede considerarse defectuoso ya que ‘\0’ podría ser un carácter válido dentro de la cadena (esto no es ‘C’). Además, no sé exactamente cómo interactúa esto con el modo de ‘texto’ de Microsoft, donde las líneas de texto generalmente terminan con dos caracteres. Si entiendo los documentos, se dejaría un ‘\r’ en la cadena porque is.getline() está “sin formato”.

    –Brent Bradburn

    17 de febrero de 2014 a las 4:54

  • if ( !str.empty() ) str.resize(str.size()-1);

    –Brent Bradburn

    17 de febrero de 2014 a las 5:00

  • try { my::getline( is, 4096, s, '\n' ); } catch ( std::ios::failure const & ) {}

    –Brent Bradburn

    21 de abril de 2016 a las 17:19

Según los comentarios y las respuestas, parece haber tres enfoques:

  1. Escribir una versión personalizada de getline() posiblemente usando el std::istream::getline() miembro internamente para obtener los caracteres reales.
  2. Utilice un búfer de flujo de filtrado para limitar la cantidad de datos potencialmente recibidos.
  3. En lugar de leer un std::stringutilice una creación de instancias de cadena con un asignador personalizado que limite la cantidad de memoria almacenada en la cadena.

No todas las sugerencias venían con código. Esta respuesta proporciona código para todos los enfoques y un poco de discusión de los tres enfoques. Antes de entrar en los detalles de la implementación, primero vale la pena señalar que hay múltiples opciones de lo que debería suceder si se recibe una entrada demasiado larga:

  1. La lectura de una línea demasiado larga podría resultar en una lectura exitosa de una línea parcial, es decir, la cadena resultante contiene el contenido leído y la transmisión no tiene ningún indicador de error configurado. Sin embargo, hacerlo significa que no es posible distinguir entre una línea que llega exactamente al límite o que es demasiado larga. Sin embargo, dado que el límite es algo arbitrario, probablemente no importe realmente.
  2. Leer una línea demasiado larga podría considerarse un error (es decir, configurar std::ios_base::failbit y/o std::ios_base::bad_bit) y, dado que la lectura falló, produce una cadena vacía. Obviamente, producir una cadena vacía evita potencialmente mirar la cadena leída hasta ahora para posiblemente ver qué está pasando.
  3. La lectura de una línea demasiado larga podría proporcionar la lectura de línea parcial y también establecer indicadores de error en la secuencia. Este parece ser un comportamiento razonable tanto para detectar que hay algo mal como para proporcionar información para una posible inspección.

Aunque hay varios ejemplos de código que implementan una versión limitada de getline() ya, aquí hay otro! Creo que es más simple (aunque posiblemente más lento; el rendimiento se puede tratar cuando sea necesario) que también retiene std::getline()interfaz s: usa la transmisión width() para comunicar un límite (tal vez tomando width() en cuenta es una extensión razonable de 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;
}

esta versión de getline() se usa igual std::getline() pero cuando parece razonable limitar la cantidad de datos leídos, el width() se establece, por ejemplo:

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

Otro enfoque es simplemente usar un búfer de flujo de filtrado para limitar la cantidad de entrada: el filtro solo contaría la cantidad de caracteres procesados ​​y limitaría la cantidad a una cantidad adecuada de caracteres. En realidad, este enfoque es más fácil de aplicar a una secuencia completa que a una línea individual: al procesar solo una línea, el filtro no puede obtener búferes llenos de caracteres de la secuencia subyacente porque no hay una forma confiable de volver a colocar los caracteres. Implementar una versión sin búfer sigue siendo simple pero probablemente no particularmente eficiente:

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

Este búfer de flujo ya está configurado para insertarse en la construcción y eliminarse en la destrucción. Es decir, se puede usar simplemente así:

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

También sería fácil agregar un manipulador que establezca el límite. Una ventaja de este enfoque es que no es necesario tocar nada del código de lectura si el tamaño total de la transmisión pudiera limitarse: el filtro podría configurarse inmediatamente después de crear la transmisión. Cuando no hay necesidad de retirar el filtro, el filtro también podría usar un búfer que mejoraría en gran medida el rendimiento.

El tercer enfoque sugerido es utilizar un std::basic_string con un asignador personalizado. Hay dos aspectos que son un poco incómodos en el enfoque del asignador:

  1. La cadena que se lee en realidad tiene un tipo que no se puede convertir inmediatamente a std::string (aunque tampoco es difícil hacer la conversión).
  2. El tamaño máximo de la matriz se puede limitar fácilmente, pero la cadena tendrá un tamaño más o menos aleatorio más pequeño que eso: cuando la secuencia falla al asignar, se lanza una excepción y no hay ningún intento de hacer crecer la cadena en un tamaño más pequeño.

Aquí está el código necesario para un asignador que limita el tamaño asignado:

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

El asignador se usaría de esta manera (el código se compila correctamente con una versión reciente de sonido metálico pero no con CCG):

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 resumen, existen múltiples enfoques, cada uno con su propio pequeño inconveniente, pero cada uno razonablemente viable para el objetivo declarado de limitar los ataques de denegación de servicio basados ​​en líneas demasiado largas:

  1. Usar una versión personalizada de getline() significa que el código de lectura debe cambiarse.
  2. El uso de un búfer de transmisión personalizado es lento, a menos que se pueda limitar el tamaño de toda la transmisión.
  3. El uso de un asignador personalizado brinda menos control y requiere algunos cambios en el código de lectura.

¿Ha sido útil esta solución?