El blog de SaToNiO

13Apr/1116

Subida de archivos segura con PHP

El otro día vi un buen artículo sobre cómo programar una subida de archivos de forma que no entrañe riesgos sobre la seguridad de la página web. Y voy a pasar a traducirlo con un bonito añadido al final.

Supongo que no está muy bien traducido, pero tampoco quiero dedicarle mucho más tiempo a mejorarlo así que si alguien quiere que cambie alguna parte, puede poner un comentario con su sugerencia.

Los blockquotes entre comienzo y fin indican comentarios mios y no algo del original.

Comienzo:

La subida de ficheros a nuestro sitio web es una tarea muy común últimamente. Normalmente querremos dejar a los usuarios subir imágenes o ficheros comprimidos con zip. Sin embargo, nunca querremos dejarles subir ficheros .php, .exe u otros scripts. Estoy seguro de que eres como yo, a quien le gustaría que su sistema de gestión de subidas de ficheros es suficientemente seguro para prevenir ataques. En este artículo voy a intentar listar las formas más seguras de proteger tu servidor y tu negocio de esta amenaza potencial. Por otro lado, siéntete libre de compartir tu experiencia con los lectores y conmigo sobre los consejos de seguridad que tengas.

Verificación del content-type

Comprobar el content-type de un fichero es el primer nivel de verificación que muchos de nosotros haremos.

<?php
// la forma más fácil de verificar que es una imagen
if(!eregi('image/', $_FILES['hpt_files']['type']))
{
    echo 'Please upload a valid file!';
    exit(0);
}
?>

Sin embargo, esto es algo que puede ser fácilmente sobrepasado por un atacante cambiando el header content-type que buscaremos después. No obstante, es algo que siempre debemos comprobar. Ten en cuenta que distintos tipos MIME podrían ser distintos en distintos navegadores.

Verificación del contenido de la imagen

Subir una imagen es algo que la mayoría de las aplicaciones permitirán. Un atacante puede cambiar el Content-Type para ser uno válido para que tu script acepte el fichero, así que nos tendremos que asegurar de que es realmente una imagen usando getimagesize() en PHP.

<?php
$imageinfo = getimagesize($_FILES['uploadfile']['tmp_name']);
if($imageinfo['mime'] != 'image/gif' && $imageinfo['mime'] != 'image/jpeg' && isset($imageinfo))
{
    echo 'Sorry, we only accept GIF and JPEG images\n';
    exit(0);
}
?>

Podrías querer comprobar también otra información. Sin embargo, un archivo puede ser al mismo tiempo una imagen válida y un script php. La mayoría de los formatos de imagen permiten comentarios de texto. ¿Cómo? tomando una imagen .jpg y subiéndola con extensión php. Cuando getimagesize la mira, ve una imagen, pero cuando el intérprete de php la mira, ejecutará comentario en php y descartará el resto como "basura binaria". Así que getimagesize da sólo un cierto nivel de verificación mientras tenemos que hacer mucho más para proteger completamente nuestro script.

Si alguien quiere más información sobre este tema, puede mirar uno dos

Comprobar la extensión del fichero

Esto es algo que todo gestor de subidas en php debe hacer. Un atacante puede falsear el content-type de un fichero al servidor, la extensión debe ser una válida para que la máquina de PHP lo interprete correctamente. Aunque esto no definitivo, es una comprobación importante. He incluido una lista blanca y otra negra en el código aunque sólo una de ellas es requerida normalmente porque no sabemos qué ocurrirá con la configuración del servidor especialmente en un servidor de alojamiento compartido.

<?php
$filename = strtolower($_FILES['uploadfile']['name']);
$whitelist = array('jpg', 'png', 'gif', 'jpeg'); //example of white list
$backlist = array('php', 'php3', 'php4', 'phtml','exe'); //example of black list
if(!in_array(end(explode('.', $fileName)), $whitelist))
{
    echo 'Invalid file type';
    exit(0);
}
if(in_array(end(explode('.', $fileName)), $backlist))
{
    echo 'Invalid file type';
    exit(0);
}
?>

De esta forma aunque el atacante cambie el content-type, no será capaz de cambiar el hecho de que se necesita otra extensión para que el intérprete de PHP ejecute el código. Sin embargo, que extensiones serán ejecutadas como código php dependerá de la configuración del servidor. Un desarrollador no tendrá conocimiento o control sobre la configuración del servidor. Algunas aplicaciones requieren que los ficheros con extensión gif o jpg sean interpretados por php así que cualquier comentario en la imagen sería ejecutado como código php.

Básicamente, no podemos garantizar que conociendo las extensiones que están siendo interpretadas por PHP eliminará todos los ataques. Además la configuración podría cambiar en algún momento en el futuro cuando se instale otra aplicación en el servidor.

La carpeta de subida

Queremos prevenir que los usuarios ejecuten los ficheros subidos directamente. Esto significa que el mejor sitio para tener esos ficheros es fuera del "web root" (www, public_html, etc) o creando un directorio en él pero restringiendo el acceso a él en la configuración del apache o en el archivo .htaccess. Si el atacante es capaz de subir algo peligroso a tu sistema, esto impedirá que lo ejecute y pueda meter arbitrariamente código en el sistema porque es incapaz de acceder al lugar. Considera el siguiente ejemplo,

<?php
$upload_dir = '/var/domainame/uploads/'; // Outside of web root
$upload_file = $uploaddir . basename($_FILES['uploadfile']['name']);
if (move_uploaded_file($_FILES['uploadfile']['tmp_name'], $uploadfile))
{
    echo 'Upload Successfully.';
    exit(0);
}
else
{
    echo 'Upload Fail';
    exit(0);
}
?>

Esto es algo bueno pero ahora el servidor web no será capaz de leer tampoco el directorio. Así que tenemos que utilizar otro fichero para que el servidor web pueda acceder y mostrar el fichero si es necesario.

<?php
$upload_dir = '/var/domainame/uploads/'; // Outside of web root
$name = $_GET['name'];
readfile($uploaddir.$name);
?>

Ahora, el usuario y el sistema serán capaces de acceder al directorio conociendo el nombre del fichero. Sin embargo, el código anterior tiene una vulnerabilidad transversal que un usuario malintencionado podría user para leer cualquier fichero legible en el sistema. Considera el siguiente ejemplo,

http://www.example.com/readfile.php?name=../secret/passwd

Esto lo más seguro es que devuelva la contraseña almacenada en el servidor. Así que siempre acuérdate de hacer seguros los POST y GET en tus scripts PHP.

El método PUT de ISS

Si estás utilizando php en Microsoft ISS, tienes que tener cuidado en tus carpetas web en las que se puede escribir. En contraposición con Apache, Microsoft ISS soporta el método PUT, que permite a los usuarios subir ficheros directamente, sin utilizar una página de subida de PHP. Sin embargo, las peticiones PUT sólo pueden ser usadas para subir ficheros a un directorio si el sistema de permisos del sistema de ficheros lo permite y los permisos de ISS también.

Para prevenir esto, tenemos que asegurarnos de que los permisos de ISS no nos permiten escritura aunque tendremos que permitirla en el directorio para poder subir archivos con un script PHP. Esto hará que una de las dos condiciones falle y esto deshabilitará PUT, que no comprobará todas las cosas que nosotros comprobaremos en nuestro script PHP y pondrá el fichero directamente en el directorio.

La función include

Esto es válido también para require, require_once e include_once, y supongo que habrá más.

En algunos scripts tenemos tendencia a utilizar un valor recibido de los usuarios para determinar qué fichero incluir en un script de php. Esto normalmente no es una buena idea puesto que el atacante podría ejecutar un fichero en el servidor. Considera el siguiente ejemplo,

<?php
// ... some code here
if(isset($_COOKIE['lang']))
    $lang = $_COOKIE['lang'];
elseif (isset($_GET['lang']))
    $lang = $_GET['lang'];
elseif (isset($_GET['lang']))
    $lang = $_GET['lang'];
else
    $lang = 'english';
 
include('language/$lang.php');
// ... some more code here
?>

Asumiendo que ningún filtrado se ha hecho en los datos recibidos, determinamos el idioma y incluimos el fichero de idioma lo que es un código común para algunos de vosotros. Un atacante podría utilizar esta brecha y utilizar una dirección para ejecutar un determinado fichero en el sistema, así que es importante hacer segura tu subida de archivos para impedir que un atacante pudiera ejecutar algún fichero que pudiera ser peligroso para tu sistema (imagina que son capaces de subir un shell o ejecutar un comando y activarlo por la URL).

Nombre de fichero aleatorio

Hablamos sobre cómo un nombre de fichero no debe ser accedido directamente por los usuarios para prevenir la ejecución de alguna forma de ataque. sin embargo, podemos todavía acceder a esos ficheros indirectamente con la ayuda de otro script. Pero si el ataque no conoce el nombre del fichero que ha subido, podría no ser capaz de ejecutar ese código arbitrario en tu servidor web. Así que es una buena idea renombrar tus ficheros con md5 u otro algoritmo de encriptación. Considera el siguiente ejemplo,

<?php
$filename = $_FILES[$uploadfile]['name'];
$save_path = '/var/domainame/uploads/'; // Outside of web root
$extension = end(explode('.', $filename)); // extension of the file
$renamed = md5($filename. time());      // rename of the file
if (!@move_uploaded_file($_FILES[$uploadfile]['tmp_name'], $save_path.$renamed. $extension))
{
    echo 'File could not be saved.';
    exit(0);
}
?>

Sin embargo, si la subida es hecha por ti mismo con una función, renombrar estos ficheros podría no ser bueno para los intereses SEO. Así que esta medida de seguridad es para las funciones de subida que permiten a los visitantes o usuarios externos subir ciertos ficheros en tu servidor web (basicamente no confias en otros que no seas tú mismo).

Disable Script Execution

También puedes intentar desactivar la ejecución de scripts en la carpeta en la que irán tus ficheros. Puedes hacer esto escribiendo un .htaccess en la carpeta.
You can also try to disabled script execution on the uploaded folder where all the files go. You can do this by writing a .htacess file on the folder.

AddHandler cgi-script .php .php3 .php4 .phtml .pl .py .jsp .asp .htm .shtml .sh .cgi
Options -ExecCGI

Esto te da una capa extra de protección. También puedes restringir que un fichero sea colocado en la carpeta y solamente permitir determinados ficheros en la carpeta. Pero recuerda que si alguna aplicación permite que tu lista blanca sea ejecutada por PHP, las posibilidades de esta protección podrían no ser muy útiles. Sin embargo, todavía sirve como una de muchas capas de protección en tu servidor.

Restringir el tamaño de los ficheros

Aunque no todos los navegadores lo soporten, algunos sí lo hacen. Esto puede dar un cierto grado de protección contra las restricciones de subida.


Debemos también restringir la subida de ficheros en php para impedir que un fichero demasiado largo cause daño a nuestro servidor (cualquier ataque puede producir un grave daño de todas formas). Comprobar el tamaño del fichero puede ayudar a miminizar la cantidad de disco que se necesita para el servidor.

<?php
//check for appropriate size with php.ini
$POST_MAX_SIZE = ini_get('post_max_size');
$mul = substr($POST_MAX_SIZE, -1);
$mul = ($mul == 'M' ? 1048576 : ($mul == 'K' ? 1024 : ($mul == 'G' ? 1073741824 : 1)));
if ($_SERVER['CONTENT_LENGTH'] > $mul*(int)$POST_MAX_SIZE && $POST_MAX_SIZE) $error = true;
$max_file_size_in_bytes = 2147483647;               // 2GB in bytes
if(!$error)
{
    //restrict the limit
    $file_size = @filesize($_FILES[$upload_name]['tmp_name']);
    if (!$file_size || $file_size > $max_file_size_in_bytes) {
        HandleError('File exceeds the maximum allowed size');
        exit(0);
    }
}
else
{
    HandleError('File exceeds the maximum allowed size in php.ini');
    exit(0);
}
?>

Puedes visitar la página de manejo de subidas en PHP para más información.

Limitar la subida de ficheros

Los ataques DOS (Denegación del servicio) podrían ser una de las preocupaciones que tengas. Los usuarios podrían ser capaces de subir un montón de ficheros grandes y consumir todo el espacio en disco disponible impidiendo que otros usuarios usaran el servicio. Así que debemos imponer alguna restricción para impedir que ocurran estos casos. El diseñador de la aplicación podría querer implementar un límite en el número de ficheros y su tamaño que un usuario puede subir en un determinado periodo de tiempo.

Almacenamiento BLOB

Una alternativa a guardar ficheros en el sistema de ficheros es guardar sus datos directamente en la base de datos como BLOB. Este método tiene la ventaja de que todo lo relacionado con la aplicación se guarda bajo el directorio principal o en la base de datos. Sin embargo esto no será probablemente una buena opción si los ficheros son grandes o si la velocidad es algo crítico.

Comprobar la sesión

Podrías querer imponer una cierta medida de seguridad teniendo una sesión entre el formulario de subida del fichero y el gestor de subidas para asegurarte de que el usuario está identificado para proceder con la subida.

Verificar la subida

Debemos también comprobar que de hecho hay un fichero para ser subido al servidor para utilizar el script gestor de subidas.

<?php
if (!isset($_FILES[$upload_name])) {
    echo 'No upload found in \$_FILES for ' . $upload_name;
    exit(0);
} else if (isset($_FILES[$upload_name]['error']) && $_FILES[$upload_name]['error'] != 0) {
    echo $uploadErrors[$_FILES[$upload_name]['error']];
    exit(0);
} else if (!isset($_FILES[$upload_name]['tmp_name']) || !@is_uploaded_file($_FILES[$upload_name]['tmp_name'])) {
    echo 'Upload failed is_uploaded_file test.';
    exit(0);
} else if (!isset($_FILES[$upload_name]['name'])) {
    echo 'File has no name.';
    exit(0);
}
?>

El anterior es un ejemplo de verificar si hay un fichero subido y si es seguro para proceder con el fichero que el usuario ha subido.

Subir un fichero en www

¿No quieres que tu carpeta esté fuera de www o public_html? Hay otra solución para esto. Sin embargo podrías necesitar un servidor dedicado o un vps al que tengas acceso a root para que funcione. En vez de dar permisos al usuario, se los damos al apache en su lugar. Puedes hacer esto con chown al directorio escribible a apache o nobody y asignar permisos 700.

Básicamente, esto deshabilitará el acceso público del fichero en el directorio. Los usuarios externos no serán capaces de ejecutar, escribir o leer en el directorio, sólo apache está permitido ya que es el dueño del fichero.

Diría que esto va a causar más problemas que soluciones. Dar permisos a www-data o similares para cambiar los archivos hará que cualquiera que pueda ejecutar algo con el servidor apache pueda leerlos también. PHP trata de resolver este problema con safe_mode pero si está habilitado un módulo de perl o algo similar vamos a tener los mismos problemas y nuestros amigos que comparten el hosting serán capaces de leer nuestras contraseñas de acceso a la base de datos en texto plano etc.

Creo que la mejor manera de solucionar esto es con la utilización de un suPHP o suexec, pero tiene que ser el hosting el que lo ponga.

De todas formas, esto es otro tema, y supongo que podría dar mucho más que hablar. Aquí nos centramos en la subida de ficheros y creo que esto está un poco fuera del tema.

Mi gestor de subidas de ficheros

Este es el gestor en el que suelo confiar en el que podríais estar interesados.

<?php
    //check for session
    if (isset($_POST['PHPSESSID']))
        session_id($_POST['PHPSESSID']);
    else if (isset($_GET['PHPSESSID']))
        session_id($_GET['PHPSESSID']);
    else
    {
        HandleError('No Session was found.');
    }
    session_start();
// Check post_max_size (http://us3.php.net/manual/en/features.file-upload.php#73762)
    $POST_MAX_SIZE = ini_get('post_max_size');
    $unit = strtoupper(substr($POST_MAX_SIZE, -1));
    $multiplier = ($unit == 'M' ? 1048576 : ($unit == 'K' ? 1024 : ($unit == 'G' ? 1073741824 : 1)));
 
    if ((int)$_SERVER['CONTENT_LENGTH'] > $multiplier*(int)$POST_MAX_SIZE && $POST_MAX_SIZE)
        HandleError('POST exceeded maximum allowed size.');
 
// Settings
    $save_path = getcwd() . '/uploads/';                // The path were we will save the file (getcwd() may not be reliable and should be tested in your environment)
    $upload_name = 'Filedata';                          // change this accordingly
    $max_file_size_in_bytes = 2147483647;               // 2GB in bytes
    $whitelist = array('jpg', 'png', 'gif', 'jpeg');    // Allowed file extensions
    $backlist = array('php', 'php3', 'php4', 'phtml','exe'); // Restrict file extensions
    $valid_chars_regex = 'A-Za-z0-9_-\s ';// Characters allowed in the file name (in a Regular Expression format)
 
// Other variables
    $MAX_FILENAME_LENGTH = 260;
    $file_name = '';
    $file_extension = '';
    $uploadErrors = array(
        0=>'There is no error, the file uploaded with success',
        1=>'The uploaded file exceeds the upload_max_filesize directive in php.ini',
        2=>'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form',
        3=>'The uploaded file was only partially uploaded',
        4=>'No file was uploaded',
        6=>'Missing a temporary folder'
    );
 
// Validate the upload
    if (!isset($_FILES[$upload_name]))
        HandleError('No upload found in \$_FILES for ' . $upload_name);
    else if (isset($_FILES[$upload_name]['error']) && $_FILES[$upload_name]['error'] != 0)
        HandleError($uploadErrors[$_FILES[$upload_name]['error']]);
    else if (!isset($_FILES[$upload_name]['tmp_name']) || !@is_uploaded_file($_FILES[$upload_name]['tmp_name']))
        HandleError('Upload failed is_uploaded_file test.');
    else if (!isset($_FILES[$upload_name]['name']))
        HandleError('File has no name.');
 
// Validate the file size (Warning: the largest files supported by this code is 2GB)
    $file_size = @filesize($_FILES[$upload_name]['tmp_name']);
    if (!$file_size || $file_size > $max_file_size_in_bytes)
        HandleError('File exceeds the maximum allowed size');
 
    if ($file_size <= 0)
        HandleError('File size outside allowed lower bound');
// Validate its a MIME Images (Take note that not all MIME is the same across different browser, especially when its zip file)
    if(!eregi('image/', $_FILES[$upload_name]['type']))
        HandleError('Please upload a valid file!');
 
// Validate that it is an image
    $imageinfo = getimagesize($_FILES[$upload_name]['tmp_name']);
    if($imageinfo['mime'] != 'image/gif' && $imageinfo['mime'] != 'image/jpeg' && $imageinfo['mime'] != 'image/png' && isset($imageinfo))
        HandleError('Sorry, we only accept GIF and JPEG images');
 
// Validate file name (for our purposes we'll just remove invalid characters)
    $file_name = preg_replace('/[^'.$valid_chars_regex.']|\.+$/i', '', strtolower(basename($_FILES[$upload_name]['name'])));
    if (strlen($file_name) == 0 || strlen($file_name) > $MAX_FILENAME_LENGTH)
        HandleError('Invalid file name');
 
// Validate that we won't over-write an existing file
    if (file_exists($save_path . $file_name))
        HandleError('File with this name already exists');
 
// Validate file extension
    if(!in_array(end(explode('.', $file_name)), $whitelist))
        HandleError('Invalid file extension');
    if(in_array(end(explode('.', $file_name)), $backlist))
        HandleError('Invalid file extension');
// Rename the file to be saved
    $file_name = md5($file_name. time());
 
// Verify! Upload the file
    if (!@move_uploaded_file($_FILES[$upload_name]['tmp_name'], $save_path.$file_name)) {
        HandleError('File could not be saved.');
    }
    exit(0);
 
/* Handles the error output. */
function HandleError($message) {
    echo $message;
    exit(0);
}
?>

Conclusión

Esto no es una solución a toda prueba para tus subidas de archivos. Sin embargo, puede ser utilizado como referencia y también discutir sobre cómo mejorar la seguridad general de nuestras aplicaciones web hoy en día.

Fin

La nueva vulnerabilidad

Bueno, ahora que hemos llegado al fin del artículo traducido, comentaré que en el afán de asegurar las subidas de ficheros, ha introducido una nueva vulnerabilidad.

Si en otro punto de la aplicación utilizamos sesiones para identificación o algún otro tipo de tarea importante, podría ser útil para un atacante fijar la sesión a un valor conocido, y posteriormente usarlo.

Ejemplo: un foro en el que se usen sesiones (casi seguro que hay formas más fáciles de atacarlo dependiendo de la página y la situación).
Eres el atacante y tienes un blog por ejemplo, y conoces a la víctima. Colocas cierto código en la página, que abra la página de subida de ficheros una vez para fijar la sesión, y si hay una función de recuérdame, a alguna de las páginas que lo implementan. La sesión se habrá fijado a un valor conocido que nosotros hemos determinado (por ejemplo entrando nosotros y obteniendo el valor de las cookies), y en el caso de haber una función de recuérdame, también estará identificada con el nuevo usuario. Si la página no hace complejos chequeos entre los que se incluye que IP no varía (este es tal vez el menos complejo), podríamos simplemente entrar en la página web con esa sesion que nosotros habíamos generado identificados con las credenciales de nuestro visitante.

Si no existe función de recuérdame, entonces aún podríamos poner un enlace todo en plan poniendo que importante es que abras esto, entrarán, verán que no están identificados y se identificarán. Si no hay mucha protección extra estaremos dentro también cuando lo hagan.

Más información sobre esto último

Comments (16) Trackbacks (0)
  1. Muy bueno tu articulo, era lo que estaba buscando.
    Tengo una duda, en tu articulo tratas acerca de la subida de imágenes, pero qué medidas se pueden tomar cuando ademas de imágenes el usuario debe subir pdf’s?
    Muchas gracias!

  2. Es necesario dejar previamente la carpeta con permisos de escritura 0777? ya que he estado buscando una forma de subir archivos con carpeta 0755 y sólo al subir el archivo abrirle los permisos y en la siguiente línea cerra los permisos? tu sabes si existe tal opción?

    Ya he intentado con el chmod(), pero entiendo que es sólo par archivos y no para carétas ó ficheros, a la vez utilicé el ftp_connect() y tampoco….

    Quiero evitar un ataque más de inyección de SPAM a través de las carpetas con permisos 0777.

  3. hola yo tengo que hacer algo como este formulario que esta aki name,email,website este cuadro donde escribes y en ves de que diga submit enviar pero en php

  4. Excelente Post:
    Algunas recomendaciones,
    1)eregi está deprecada usa preg_match en su lugar
    2)La superglobal $_SERVER[‘CONTENT_LENGTH’] no siempre está disponible.
    3)La blacklist no es segura conviene siempre filtrar por lista blanca.

  5. Buenísimo el artículo!!

  6. Ei!!!
    Muy bueno el articulo al verlo tenia que comentar para por lo menos darte las gracias.

    GRACIAS!

    Saludos

  7. Hi to all, the contents present at this site are actually awesome for people experience, well, keep up the nice work fellows.

  8. Hola, no queda claro como mostrar en la pantalla la imagen subida fuera de htdocs. Como seria el codigo completo para mostrar la imagen?

  9. Hola buen día, tengo un problema con los procesos de php en el cpanel, casi siempre está en 96/100 y me está dando muchos dolores de cabeza, identificando errores me dice que el error está en la intranet, home/kacosaco/public_html/kacosa.com/reportes/imagenes/subidas/
    y el error que me da el cpanel es [Wed Oct 07 09:25:09.081772 2015] [autoindex:error] [pid 764076:tid 139909518018304] [client 201.211.123.249:63321] AH01276: Cannot serve directory /home/kacosaco/public_html/kacosa.com/reportes/imagenes/subidas/: No matching DirectoryIndex (index.html.var,index.htm,index.html,index.xhtml,index.wml,index.perl,index.pl,index.plx,index.ppl,index.cgi,index.jsp,index.js,index.jp,index.php4,index.php3,index.php,index.phtml,index.shtml,default.htm,default.html,home.htm,index.php5,Default.html,Default.htm,home.html,welcome.html) found, and server-generated directory index forbidden by Options directive, referer: http://kacosa.com/reportes/archivos.php?seccion=ver_articulo&codigo=270

    y asi me salen decenas de errores que me consumen toda la memoria del host.

    A la espera de tu respuesta y agradecido de antemano.

  10. ???????????????????????
    ?????????????????????????????????????????????????
    ??????????????????????

    ?????????????????????????????????
    ?????????????????????????????????/?????????
    ???????????????????????

    ?????????????????????????????????????????????????????????????????
    ????????????????????????????????????????

    ???????????????????
    ????????????????????
    ???????????????

  11. This is a really intieleglnt way to answer the question.

  12. Good review. It seems to have omitted one significant difference between the Swimsense and both Garmins, and that is the auto pause feature on the Swimsense. I have all my swim and rest intervals tracked without having to push the pause button. The display is not great but the auto pause feature still makes it worth keeping.

  13. I can already tell that’s gonna be super helpful.

  14. You’re back!!! Yay!! You were missed. I can’t imagine ever working where no one asked how my evening/ weekend was. It would drive me batty. Glad you’re happy with how everything is working out. I too am curious about the special project.5/5 0/3 3/3

  15. I rarely use the little cheap blue eyeliner I have. Sometimes I do use it as an accent on the outer corner or just a little underneath the eye as black tends to be too heavy there.Hope you're feeling well.

  16. Ja klar, nur so geht das…nur so kann man etwas bewegen und nicht auf die Schwätzer hören!!! Noch ist dies hier ein demokratisches Land wo jeder seine Meinung äussern kann…ob Jugentliche, Rentner, Arbeitslose oder Unternehmer!!! Oben bleiben!


Leave a comment

No trackbacks yet.