La gestión de ficheros es siempre una tarea delicada, ya que se presta a ataques locales de "race condition". Se produce una "race condicion" cuando varios procesos están compitiendo por un recurso, y entre dos operaciones de uno de ellos se le cuela otro que cambia las características del recurso. Supongamos, por ejemplo, el caso siguiente:
if(access("/path/del/fichero",W_OK)
0) open(...);
El código anterior verifica que el usuario que ejecuta el programa tenga acceso de escritura al fichero en cuestión. Si es el caso, el open lo abre para escritura y graba en él.
Existe, sin embargo, una "race condition". Supongamos que un usuario malicioso crea un fichero válido, ejecuta el código anterior y, entre el "access()" y el "open()", borra el fichero y crea un enlace, pongamos, a "/etc/passwd". Las consecuencias pueden ser catastróficas. Evidentemente hace falta mucha casualidad para que las manipulaciones del usuario tengan lugar en el momento oportuno, pero no hay nada que le impida crear un proceso que lo intente repetidamente hasta tener éxito. A un ordenador no le cuesta nada intentarlo durante una semana seguida.
Es por ello que "access()" es una función anticuada que no debería ser utilizada nunca, y menos en procesos SUID. En su lugar deben utilizarse funciones como "stat()","lstat()" y "fstat()", como se verá dentro de un momento. Lamentablemente estas funciones carecen de la propiedad de "access()" que permite validar el acceso en función del usuario real del proceso, no del usuario efectivo. Ello la hace muy conveniente en procesos SUID.
La forma correcta de manejar un fichero es la siguiente:
- Apertura de un fichero ya existente, como lectura o como "append":
lstat()
Vemos si es aceptable (enlace simbólico, dispositivo, fichero regular, etc).
open()
fstat()
Comparar el "lstat()" con el "fstat()". Si son diferentes es que alguien ha tocado el fichero mientras tanto.
- Creación de un fichero nuevo:
open(O_CREAT|O_EXCL)
lstat()
Vemos si es aceptable (enlace simbólico, dispositivo, fichero regular, etc).
open()
fstat()
Comparar el "lstat()" con el "fstat()". Si son diferentes es que alguien ha tocado el fichero mientras tanto.
ftruncate()
La forma más sencilla consiste en
setegid(GID real)
seteuid(UID real)
unlink()
seteuid(UID privilegiado)
setegid(GID privilegiado)
En cualquier caso es preciso verificar que el usuario real (no el efectivo) del proceso tenga permiso para efectuar esa operación. Se puede hacer un "setegid()", "seteuid()" al usuario real antes de la operación y volver a usuario SUID de nuevo, de la misma forma, una vez validado el acceso. Otra posibilidad es crear una "pipe", hacer un "fork()" y que sea el hijo, previos "setgid()" y "setuid()", quien acceda al fichero. Ello es especialmente importante si en el "path" hay directorios, ya que de otra forma se nos obligaría a verificar componente a componente. Ese es precisamente el reciente problema de seguridad de los servidores web bajo Windows NT y el "::$DATA".
En los sistemas no POSIX que no tienen la opción de "saved uid/gid" se puede utilizar la llamada
setreuid(geteuid(),getuid());
para intercambiar el usuario real y efectivo.
Como regla general, nunca crees o leas ficheros en directorios en los que pueda escribir todo el mundo. Y cuando crees ficheros, hazlo con los privilegios más restringidos que sea posible. Debemos recordar también que la verificación del acceso se realiza en el "open()", no en los "read()" y "write()". Ello permite, por ejemplo, abrir un fichero mientras somos privilegiados, despojarnos de los privilegios y seguir escribiendo en él.