¿Cómo puedo asegurarme de que una variable contenga solo un nombre de archivo válido?

8

Dado el siguiente script, ¿cómo puedo asegurarme de que el argumento solo contenga un nombre de archivo válido dentro /home/charlesingalls/y no una ruta ( ../home/carolineingalls/) o comodín, etc.

Solo quiero que el script pueda eliminar un solo archivo del directorio codificado. Este script se ejecutará como un usuario privilegiado.

#!/bin/bash

rm -f /home/charlesingalls/"$1"
Aaron Cicali
fuente
2
Si no desea admitir "foo / bar", simplemente verifique que no contenga /. Los comodines no se interpretan entre comillas.
Random832
3
Si está eliminando un archivo , no lo use -rcon rm. rm -res para la eliminación recursiva de un directorio y todos los archivos y directorios debajo de él. Solo es útil al eliminar directorios. Más en general, no cultive el cargamento es decir, no solo copie cosas que parecen útiles en su línea de comandos o script sin comprender lo que hacen o cómo funcionan. Los dioses del avión que traen la carga mágica pueden enojarse y eliminar todos sus archivos.
cas
Buen punto con respecto a la bandera -r: entiendo su uso, simplemente no estaba pensando con claridad.
Aaron Cicali

Respuestas:

7

Si solo desea eliminar un archivo /home/charlesingalls(y no un archivo en un subdirectorio), entonces es fácil: simplemente verifique que el argumento no contenga a /.

case "$1" in
  */*) echo 1>&2 "Refusing to remove a file in another directory"; exit 2;;
  *) rm -f /home/charlesingalls/"$1";;
esac

Esto se ejecuta rmincluso si el argumento es .o está ..vacío, pero en ese caso rmfallará de forma inofensiva en eliminar un directorio.

Los comodines no son relevantes aquí ya que no se realiza ninguna expansión de comodines.

Esto es seguro incluso en presencia de enlaces simbólicos: si el archivo es un enlace simbólico, el enlace simbólico (que está en /home/charlesingalls) se elimina y el destino de ese enlace no se ve afectado.

Tenga en cuenta que esto supone que /home/charlesingallsno se puede mover ni cambiar. Eso debería estar bien si el directorio está codificado en el script, pero si se determina a partir de variables, la determinación podría no ser válida para cuando se rmejecute el comando.

Con base en la información adicional de que el argumento es un nombre de host virtual, debe hacer una lista blanca en lugar de una lista negra: verifique que el nombre sea un nombre de host virtual sensible, en lugar de solo prohibir barras. Compruebo que el nombre comienza con una letra minúscula o un dígito y que no contiene caracteres que no sean letras minúsculas, dígitos, puntos y guiones.

LC_CTYPE=C LC_COLLATE=C
case "$1" in
  *[!-.0-9a-z]*|[!0-9a-z]*) echo >&2 "Invalid host name"; exit 2;;
  *) rm -f /home/charlesingalls/"$1";;
esac
Gilles 'SO- deja de ser malvado'
fuente
En este caso, los archivos son archivos de configuración del servidor web que han sido generados por un proceso anterior. Todos tienen el mismo nivel de importancia (cada uno constituye un host virtual). Sin embargo, el directorio en el que existen es adyacente a directorios similares, y todos existen en un directorio que es propiedad del servidor web. Este script en particular está destinado a permitir la eliminación de configuraciones de host virtual solo en un determinado directorio.
Aaron Cicali
¿Tiene sentido este enfoque para usted siempre que ese caso de uso? Aprecio tu experiencia y admitiré que definitivamente hay veces que "he traído una bazuca a un tiroteo" para automatizar algo en Linux.
Aaron Cicali
@AaronCicali ¿Por qué el script puede eliminar cualquiera de las configuraciones de host y no solo una que pertenece a la entidad que realizó la solicitud original? ¿Por qué no se validó primero el nombre de host virtual (luego no contendría ningún carácter especial)?
Gilles 'SO- deja de ser malvado'
La entidad que realiza la solicitud original es una GUI que de hecho tiene permiso para eliminar cualquiera de los hosts virtuales en esa carpeta. El nombre de host virtual se valida primero. Proviene de una lista de hosts virtuales creados previamente (nombres de dominio). Creo que el trabajo de este script es garantizar que sea lo más seguro posible sin depender de la seguridad de otra parte de la aplicación. Es decir, que solo puede eliminar archivos de este directorio específico. También tendrá que hacer un trabajo de limpieza adicional.
Aaron Cicali
@AaronCicali Ok, como un control de cordura adicional esto tiene sentido. En este caso, debe incluir en la lista blanca: solo acepte nombres que parezcan nombres de host plausibles. Si no permite subdominios, incluso podría prohibirlo.
Gilles 'SO- deje de ser malvado'
10

Esta respuesta supone que $1está permitido incluir subdirectorios. Si está interesado en el caso más simple donde $1debería ser un nombre de directorio simple, consulte una de las otras respuestas.


Los comodines no se expanden entre comillas dobles. Como $1está entre comillas dobles, los comodines no son un problema.

Ambos ../y los enlaces simbólicos pueden ocultar la ubicación real de un archivo. A continuación se muestran pruebas para determinar si el archivo está realmente, no solo aparentemente, bajo la ruta que queremos.

Sistemas más nuevos: usando realpath

En cuanto a averiguar si el archivo es realmente si el archivo está realmente bajo /home/charlesingalls/o no, puede usar realpath:

realpath --relative-base=/home/charlesingalls/ "/home/charlesingalls/$1"  | grep -q '^/' && exit 1

Lo anterior se ejecuta exit 1si el archivo especificado por $1está en otro lugar que no sea el directorio /home/charlesingalls/. realpathcanonicaliza todo el camino, eliminando enlaces simbólicos y ../.

realpath es parte de GNU coreutils y debería estar disponible en cualquier sistema Linux.

realpathrequiere GNU coreutils 8.15 (enero de 2012) o superior .

Ejemplos

Para demostrar cómo sigue realpath ../para determinar la ubicación real de un archivo (por ejemplo, -qse omite la opción de grep para que la salida real de grep sea visible):

$ touch /tmp/test
$ realpath --relative-base=$HOME "$HOME/../../tmp/test" | grep '^/' && echo FAIL
/tmp/test
FAIL

Para demostrar cómo sigue los enlaces simbólicos:

$ ln -s /tmp/test ~/test
$ realpath --relative-base=$HOME "$HOME/test" | grep '^/' && echo FAIL
/tmp/test
FAIL

Sistemas más antiguos: utilizando readlink -e

readlinktambién es capaz de cononicalizar una ruta, siguiendo enlaces simbólicos y ../:

readlink -e "$HOME/test" | grep -q "^$HOME" || exit 1

Usando los mismos archivos de ejemplo:

$ readlink -e "$HOME/../../tmp/test" | grep "$HOME" || echo FAIL
FAIL
$ readlink -e "$HOME/test" | grep "^$HOME" || echo FAIL
FAIL

Además de estar disponibles en sistemas GNU más antiguos, hay versiones de readlinkBSD disponibles.

John1024
fuente
Mi Ubuntu 14.04 coreutilsno tienerealpath
heemayl
1
Esto parece ser más de lo que estoy buscando, pero desafortunadamente mi servidor Centos 6.5 no tiene Realpath. No estoy en condiciones de instalarlo. Buscar en Google aparece menciones de "readlink -f" superceding realpath, pero todavía tengo que hacerlo funcionar.
Aaron Cicali
1
El subtítulo menciona -f("todos menos el último componente debe existir") y los ejemplos usan -e("todos los componentes deben existir"), lo cual es un poco confuso.
isanae
2
@AaronCicali Esta definitivamente no es la respuesta que necesita, porque es peligrosamente incorrecta. No debe resolver enlaces simbólicos . El objetivo del enlace simbólico puede cambiar entre el momento en que lo verifica y el momento en que lo usa. (Es una categoría bien conocida de errores de diseño ). Además, no tiene sentido resolver enlaces simbólicos ya que rmactúa sobre el argumento en sí, no sobre su objetivo.
Gilles 'SO- deja de ser malvado'
1
Sugerencia: Use grep -qpara evitar la grepsalida de líneas coincidentes. Todavía obtienes el estado de salida &&y ||aún así trabajas exactamente como estás acostumbrado.
un CVn
2

Si desea prohibir completamente las rutas, la forma más simple es probar si la variable contiene una barra diagonal ( /). En bash:

if [[ "$1" = */* ]] ; then...

Sin embargo, esto bloqueará todos los caminos, incluidos foo/bar. En su ..lugar, podría probar , pero eso dejaría la posibilidad de enlaces simbólicos que apuntan a directorios fuera de la ruta de destino.

Si solo desea permitir la eliminación de un solo archivo, no creo que deba usarlo rm -r.


Además, dependiendo de lo que esté haciendo, podría usar los permisos de archivos del sistema para permitir solo eliminar archivos que el usuario podría eliminar ellos mismos. Algo como esto:

su charlesingalls -c "rm /home/charlesingalls/'$1'"

Aunque, como comentó @Gilles, esto tiene un problema de $1comillas : fallará si contiene una comilla simple, por lo que la variable debe probarse primero (por ejemplo, if [[ "$1" = *\'* ]] ; then fail...o más bien mediante la inclusión en la lista blanca de un conjunto sensible de caracteres), o el nombre del archivo pasado una variable de entorno con p. ej.

file="$1" su charlesingalls -c 'rm "/home/charlesingalls/$file"'
ilkkachu
fuente
Buen punto con respecto a la bandera -r, que realmente no debería estar allí. Eliminando ahora ...
Aaron Cicali
Tenga en cuenta que su sucomando está roto porque la cita es incorrecta. Estás ejecutando un comando con el argumento interpolado como un fragmento de shell. Por ejemplo, si el argumento es $(touch foo)su código se ejecuta touch foo.
Gilles 'SO- deja de ser malvado'
@ Gilles, mierda, sabía que tenía que haber algo mal. La cita anidada no es mi amigo favorito. Creo que la versión actual funciona mejor, las comillas dobles evaluarán la variable en el shell externo y las comillas simples evitan que se evalúe en el shell interno. Sin garantía.
ilkkachu
@ilkkachu Esa versión falla si el argumento contiene una comilla simple. (Pero solo en ese caso, lo que hace que sea mucho más fácil validar que cuando se usan comillas dobles). Es imposible interpolar directamente una cadena arbitraria. Debe masajear la cadena (por ejemplo, reemplazarla 'por todas '\'') o pasarla por otro canal, como una variable de entorno (que es lo que haría aquí file_to_remove="$1" su -c 'rm "/home/charlesingalls/$file_to_remove"'). Resulta que se supone que el nombre del archivo es un nombre de host virtual, por lo que rechazar todos los caracteres especiales también estaría bien aquí.
Gilles 'SO- deja de ser malvado'