Utiliser le fichier de configuration pour mon script shell

26

J'ai besoin de créer un fichier de configuration pour mon propre script: voici un exemple:

scénario:

#!/bin/bash
source /home/myuser/test/config
echo "Name=$nam" >&2
echo "Surname=$sur" >&2

Contenu de /home/myuser/test/config:

nam="Mark"
sur="Brown"

ça marche!

Ma question: est-ce la bonne façon de procéder ou y a-t-il d'autres façons?

Pol Hallen
la source
Les variables doivent être en haut. Je suis surpris que ça marche. Quoi qu'il en soit, pourquoi avez-vous besoin d'un fichier de configuration? Envisagez-vous d'utiliser ces variables ailleurs?
Faheem Mitha
Faheem, j'ai besoin des variables car mon script a de nombreuses options: utiliser un fichier de configuration permettra de simplifier le script. Merci
Pol Hallen
5
À mon humble avis, c'est bien. Je ferais ça.
Tinti
abcdele fait également de cette façon et c'est un assez gros programme (pour un script shell). Vous pouvez y jeter un œil ici .
Lucas

Réponses:

19

sourcen'est pas sécurisé car il exécutera du code arbitraire. Cela peut ne pas vous préoccuper, mais si les autorisations de fichier sont incorrectes, il peut être possible pour un attaquant disposant d'un accès au système de fichiers d'exécuter du code en tant qu'utilisateur privilégié en injectant du code dans un fichier de configuration chargé par un script sécurisé tel qu'un script d'initialisation.

Jusqu'à présent, la meilleure solution que j'ai pu identifier est la solution maladroite de réinvention du volant:

myscript.conf

password=bar
echo rm -rf /
PROMPT_COMMAND='echo "Sending your last command $(history 1) to my email"'
hostname=localhost; echo rm -rf /

À l'aide source, cela s'exécuterait echo rm -rf /deux fois, ainsi que changer l'utilisateur en cours d'exécution$PROMPT_COMMAND . Au lieu de cela, faites ceci:

myscript.sh (Bash 4)

#!/bin/bash
typeset -A config # init array
config=( # set default values in config array
    [username]="root"
    [password]=""
    [hostname]="localhost"
)

while read line
do
    if echo $line | grep -F = &>/dev/null
    then
        varname=$(echo "$line" | cut -d '=' -f 1)
        config[$varname]=$(echo "$line" | cut -d '=' -f 2-)
    fi
done < myscript.conf

echo ${config[username]} # should be loaded from defaults
echo ${config[password]} # should be loaded from config file
echo ${config[hostname]} # includes the "injected" code, but it's fine here
echo ${config[PROMPT_COMMAND]} # also respects variables that you may not have
               # been looking for, but they're sandboxed inside the $config array

myscript.sh ( compatible Mac / Bash 3)

#!/bin/bash
config() {
    val=$(grep -E "^$1=" myscript.conf 2>/dev/null || echo "$1=__DEFAULT__" | head -n 1 | cut -d '=' -f 2-)

    if [[ $val == __DEFAULT__ ]]
    then
        case $1 in
            username)
                echo -n "root"
                ;;
            password)
                echo -n ""
                ;;
            hostname)
                echo -n "localhost"
                ;;
        esac
    else
        echo -n $val
    fi
}

echo $(config username) # should be loaded from defaults
echo $(config password) # should be loaded from config file
echo $(config hostname) # includes the "injected" code, but it's fine here
echo $(config PROMPT_COMMAND) # also respects variables that you may not have
               # been looking for, but they're sandboxed inside the $config array

Veuillez répondre si vous trouvez un exploit de sécurité dans mon code.

Mikkel
la source
1
Pour info, il s'agit d'une solution Bash version 4.0 qui est malheureusement soumise à des problèmes de licence insensés imposés par Apple et n'est pas disponible par défaut sur Mac
Sukima
@Sukima Bon point. J'ai ajouté une version compatible avec Bash 3. Sa faiblesse est qu'elle ne gérera pas *correctement les entrées, mais qu'est-ce qui, dans Bash, gère bien ce personnage?
Mikkel
Le premier script échoue si le mot de passe contient une barre oblique inverse.
Kusalananda
@Kusalananda Et si la barre oblique inversée s'est échappée? my\\password
Mikkel
10

Voici une version propre et portable compatible avec Bash 3 et plus, sur Mac et Linux.

Il spécifie toutes les valeurs par défaut dans un fichier séparé, pour éviter la nécessité d'une fonction de configuration "par défaut" énorme, encombrée et dupliquée dans tous vos scripts shell. Et il vous permet de choisir entre la lecture avec ou sans repli par défaut:

config.cfg :

myvar=Hello World

config.cfg.defaults :

myvar=Default Value
othervar=Another Variable

config.shlib (c'est une bibliothèque, donc il n'y a pas de ligne de shebang):

config_read_file() {
    (grep -E "^${2}=" -m 1 "${1}" 2>/dev/null || echo "VAR=__UNDEFINED__") | head -n 1 | cut -d '=' -f 2-;
}

config_get() {
    val="$(config_read_file config.cfg "${1}")";
    if [ "${val}" = "__UNDEFINED__" ]; then
        val="$(config_read_file config.cfg.defaults "${1}")";
    fi
    printf -- "%s" "${val}";
}

test.sh (ou tout script où vous souhaitez lire les valeurs de configuration) :

#!/usr/bin/env bash
source config.shlib; # load the config library functions
echo "$(config_get myvar)"; # will be found in user-cfg
printf -- "%s\n" "$(config_get myvar)"; # safer way of echoing!
myvar="$(config_get myvar)"; # how to just read a value without echoing
echo "$(config_get othervar)"; # will fall back to defaults
echo "$(config_get bleh)"; # "__UNDEFINED__" since it isn't set anywhere

Explication du script de test:

  • Notez que toutes les utilisations de config_get dans test.sh sont entourées de guillemets doubles. En enveloppant chaque config_get entre guillemets, nous nous assurons que le texte dans la valeur de la variable ne sera jamais mal interprété comme indicateurs. Et cela garantit que nous préservons correctement les espaces blancs, tels que plusieurs espaces consécutifs dans la valeur de configuration.
  • Et quelle est cette printfligne? Eh bien, c'est quelque chose que vous devez savoir: echoc'est une mauvaise commande pour imprimer du texte que vous n'avez aucun contrôle. Même si vous utilisez des guillemets doubles, il interprètera les drapeaux. Essayez de mettre myvar(in config.cfg) sur -eet vous verrez une ligne vide, car vous echopenserez que c'est un drapeau. Mais printfn'a pas ce problème. Le printf --dit "imprimer ceci, et n'interprète rien comme des drapeaux", et le "%s\n"dit "formater la sortie sous forme de chaîne avec un retour à la ligne de fin, et enfin le paramètre final est la valeur pour printf à formater.
  • Si vous n'allez pas faire écho de valeurs à l'écran, vous devez simplement les affecter normalement, comme myvar="$(config_get myvar)";. Si vous allez les imprimer à l'écran, je suggère d'utiliser printf pour être totalement à l'abri de toute chaîne incompatible avec l'écho qui pourrait se trouver dans la configuration utilisateur. Mais l'écho est très bien si la variable fournie par l'utilisateur n'est pas le premier caractère de la chaîne que vous faites écho, car c'est la seule situation où "drapeaux" pourrait être interprété, donc quelque chose comme echo "foo: $(config_get myvar)";est sûr, car le "foo" ne le fait pas commencez par un tiret et indique donc à echo que le reste de la chaîne n'est pas non plus un indicateur. :-)
gw0
la source
@ user2993656 Merci d'avoir remarqué que mon code d'origine contenait toujours mon nom de fichier de configuration privé (environment.cfg) au lieu du bon. Quant à l'édition "echo -n" que vous avez effectuée, cela dépend du shell utilisé. Sur Mac / Linux Bash, "echo -n" signifie "écho sans retour à la ligne", ce que j'ai fait pour éviter les retours à la ligne. Mais cela semble fonctionner exactement de la même manière, alors merci pour les modifications!
gw0
En fait, je viens de le parcourir et de le réécrire pour utiliser printf au lieu de echo, ce qui garantit que nous nous débarrasserons du risque d'écho mal interpréter les «drapeaux» dans les valeurs de configuration.
gw0
J'aime vraiment cette version. J'ai laissé tomber config.cfg.defaultsau lieu de les définir au moment de l'appel $(config_get var_name "default_value"). tritarget.org/static/…
Sukima
7

Analysez le fichier de configuration, ne l'exécutez pas.

J'écris actuellement une application au travail qui utilise une configuration XML extrêmement simple:

<config>
    <username>username-or-email</username>
    <password>the-password</password>
</config>

Dans le script shell ("l'application"), voici ce que je fais pour obtenir le nom d'utilisateur (plus ou moins, je l'ai mis dans une fonction shell):

username="$( xml sel -t -v '/config/username' "$config_file" )"

La xmlcommande est XMLStarlet , qui est disponible pour la plupart des Unices.

J'utilise XML car d'autres parties de l'application traitent également des données encodées dans des fichiers XML, donc c'était plus simple.

Si vous préférez JSON, il jqexiste un analyseur JSON shell facile à utiliser.

Mon fichier de configuration ressemblerait à ceci en JSON:

{                                 
  "username": "username-or-email",
  "password": "the-password"      
}                

Et puis j'obtiendrais le nom d'utilisateur dans le script:

username="$( jq -r '.username' "$config_file" )"
Kusalananda
la source
L'exécution du script présente un certain nombre d'avantages et d'inconvénients. Les principaux inconvénients sont la sécurité, si quelqu'un peut modifier le fichier de configuration, il peut alors exécuter du code, et il est plus difficile de le rendre idiot. Les avantages sont la rapidité, sur un simple test, il est plus de 10 000 fois plus rapide de générer un fichier de configuration que d'exécuter pq, et la flexibilité, tous ceux qui aiment le patch de singe python l'apprécieront.
icarus
@icarus Quelle est la taille habituelle des fichiers de configuration et à quelle fréquence devez-vous les analyser en une seule session? Notez également que plusieurs valeurs peuvent être issues de XML ou JSON en une seule fois.
Kusalananda
Habituellement, seules quelques valeurs (1 à 3). Si vous utilisez evalpour définir plusieurs valeurs, vous exécutez des parties sélectionnées du fichier de configuration :-).
icarus
1
@icarus Je pensais à des tableaux ... Pas besoin de evalquoi que ce soit. Les performances de l'utilisation d'un format standard avec un analyseur existant (même s'il s'agit d'un utilitaire externe) sont négligeables par rapport à la robustesse, la quantité de code, la facilité d'utilisation et la maintenabilité.
Kusalananda
1
+1 pour "analyser le fichier de configuration, ne pas l'exécuter"
Iiridayn
4

La manière la plus courante, efficace et correcte consiste à utiliser source, ou .sous forme de raccourci. Par exemple:

source /home/myuser/test/config

ou

. /home/myuser/test/config

Cependant, il faut tenir compte des problèmes de sécurité que l'utilisation d'un fichier de configuration externe supplémentaire peut soulever, étant donné que du code supplémentaire peut être inséré. Pour plus d'informations, notamment sur la façon de détecter et de résoudre ce problème, je vous recommande de consulter la section «Secure it» de http://wiki.bash-hackers.org/howto/conffile#secure_it

NFarrington
la source
5
J'avais de grands espoirs pour cet article (également apparu dans mes résultats de recherche), mais la suggestion de l'auteur d'essayer d'utiliser l'expression régulière pour filtrer le code malveillant est un exercice futile.
Mikkel
La procédure avec point, nécessite un chemin absolu? Avec le parent, cela ne fonctionne pas
Davide
2

J'utilise ceci dans mes scripts:

sed_escape() {
  sed -e 's/[]\/$*.^[]/\\&/g'
}

cfg_write() { # path, key, value
  cfg_delete "$1" "$2"
  echo "$2=$3" >> "$1"
}

cfg_read() { # path, key -> value
  test -f "$1" && grep "^$(echo "$2" | sed_escape)=" "$1" | sed "s/^$(echo "$2" | sed_escape)=//" | tail -1
}

cfg_delete() { # path, key
  test -f "$1" && sed -i "/^$(echo $2 | sed_escape).*$/d" "$1"
}

cfg_haskey() { # path, key
  test -f "$1" && grep "^$(echo "$2" | sed_escape)=" "$1" > /dev/null
}

Devrait prendre en charge toutes les combinaisons de caractères, sauf que les clés ne peuvent pas en avoir =, car c'est le séparateur. Tout le reste fonctionne.

% cfg_write test.conf mykey myvalue
% cfg_read test.conf mykey
myvalue
% cfg_delete test.conf mykey
% cfg_haskey test.conf mykey || echo "It's not here anymore"
It's not here anymore

De plus, c'est complètement sûr car il n'utilise aucun type de source/eval

therealfarfetchd
la source
0

Pour mon scénario, sourceou .était bien, mais je voulais prendre en charge les variables d'environnement locales (c'est-à-dire FOO=bar myscript.sh) en prévalant sur les variables configurées. Je voulais également que le fichier de configuration soit modifiable par l'utilisateur et confortable pour quelqu'un habitué aux fichiers de configuration, et qu'il soit aussi petit / simple que possible, afin de ne pas distraire de l'objectif principal de mon tout petit script.

Voici ce que j'ai trouvé:

CONF=${XDG_CONFIG_HOME:-~/config}/myscript.sh
if [ ! -f $CONF ]; then
    cat > $CONF << CONF
VAR1="default value"
CONF
fi
. <(sed 's/^\([^=]\+\) *= *\(.*\)$/\1=${\1:-\2}/' < $CONF)

Essentiellement - il vérifie les définitions des variables (sans être très flexible sur les espaces blancs) et réécrit ces lignes afin que la valeur soit convertie en valeur par défaut pour cette variable, et que la variable ne soit pas modifiée si elle est trouvée, comme la XDG_CONFIG_HOMEvariable ci-dessus. Il source cette version modifiée du fichier de configuration et continue.

Les travaux futurs pourraient rendre le sedscript plus robuste, filtrer les lignes qui semblent étranges ou qui ne sont pas des définitions, etc., ne pas casser les commentaires de fin de ligne - mais cela me suffit pour le moment.

Iiridayn
la source
0

C'est succinct et sûr:

# Read common vars from common.vars
# the incantation here ensures (by env) that only key=value pairs are present
# then declare-ing the result puts those vars in our environment 
declare $(env -i `cat common.vars`)

Le -igarantit que vous obtenez uniquement les variables decommon.vars

Marcin
la source
-2

Tu peux le faire:

#!/bin/bash
name="mohsen"
age=35
cat > /home/myuser/test/config << EOF
Name=$name
Age=$age
EOF
Golfe Persique
la source