diff --git a/.gitignore b/.gitignore index 1900b08..a9dd2a6 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ diagnostic.partial diagnostic.test tests/*.diff +tests/*.actual spec/ spec-runner/ node_modules/ diff --git a/README.md b/README.md index b193a09..3f0b2b0 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,18 @@ There are more scripts available in the [demos directory](demo/) that could help There are additional features that the program supports. Try using `mo --help` to see what is available. +Security +----------- + +Because `mo` uses environment variables that could be unsafe in the event that `mo` is invoked programmatically with user-input, `mo` provides `--strict` mode when used with `--source=`. `mo` will attempt to track variables that are created by each `--source=`'ed environment file, and will disallow access to external variables. + +To use this, `--strict` must be used with `--source=`: + +```bash +./mo --strict --source=.env my-template.mustache +``` + + Concessions ----------- diff --git a/mo b/mo index c928d7c..75f5dc7 100755 --- a/mo +++ b/mo @@ -95,8 +95,15 @@ # treated as an empty value for the purposes of conditionals. # MO_ORIGINAL_COMMAND - Used to find the `mo` program in order to generate a # help message. +# MO_STRICT_MODE - Used to limit access to variables outside of sourced files # # Returns nothing. + +MO_HAS_SOURCED=false +MO_STRICT_SWITCH=false +MO_STRICT_MODE=false +MO_SOURCED_VARS=() + mo() ( # This function executes in a subshell so IFS is reset. # Namespace this variable so we don't conflict with desired values. @@ -119,6 +126,10 @@ mo() ( exit 0 ;; + --strict) + MO_STRICT_SWITCH=true + ;; + --allow-function-arguments) # shellcheck disable=SC2030 MO_ALLOW_FUNCTION_ARGUMENTS=true @@ -147,8 +158,21 @@ mo() ( fi if [[ -f "$f2source" ]]; then + MO_HAS_SOURCED=true + + local BEFORE_VARS AFTER_VARS ALL_VARS + BEFORE_VARS=(`cat <(set -o posix ; set) | cut -d'=' -f1`) + # shellcheck disable=SC1090 . "$f2source" + AFTER_VARS=(`cat <(set -o posix ; set) | cut -d'=' -f1`) + + ALL_VARS=( ${BEFORE_VARS[@]} ${AFTER_VARS[@]} ) + MO_SOURCED_VARS=( \ + ${MO_SOURCED_VARS[@]} \ + $(echo "${ALL_VARS[@]}" | tr ' ' '\n' | sort | uniq -u)\ + ) + unset BEFORE_VARS AFTER_VARS ALL_VARS else echo "No such file: $f2source" >&2 exit 1 @@ -169,8 +193,17 @@ mo() ( done fi + # Allow turning off Strict Mode + if [[ $MO_HAS_SOURCED == true ]] && [[ $MO_STRICT_SWITCH == true ]]; then + MO_STRICT_MODE=true + fi + moGetContent moContent "${files[@]}" || return 1 moParse "$moContent" "" true + + for v in ${MO_SOURCED_VARS[@]}; do + unset -v $v > /dev/null 2>&1 + done ) @@ -908,6 +941,7 @@ moShow() { fi fi else + moTestLegal "${moNameParts[0]}" || return 1 # Further subindexes are disallowed eval "echo -n \"\${${moNameParts[0]}[${moNameParts[1]%%.*}]}\"" fi @@ -989,6 +1023,18 @@ moStandaloneDenied() { } +# Internal: Determines if a variable name is outside of the scope of the template +# +# Returns 0 if variable is safe, Returns 1 if variable is illegal +moTestLegal() { + [[ $MO_STRICT_MODE != true ]] && return 0 + [[ " ${MO_SOURCED_VARS[*]} " =~ " ${1%.*} " ]] && return 0 + + echo "Illegal variable access $1" >&2 + return 1 +} + + # Internal: Determines if the named thing is a function or if it is a # non-empty environment variable. When MO_FALSE_IS_EMPTY is set to a # non-empty value, then "false" is also treated is an empty value. @@ -1004,6 +1050,9 @@ moStandaloneDenied() { # Returns 0 if the name is not empty, 1 otherwise. When MO_FALSE_IS_EMPTY # is set, this returns 1 if the name is "false". moTest() { + # Don't allow access to outside names + moTestLegal "$1" || return 1 + # Test for functions moIsFunction "$1" && return 0 @@ -1030,7 +1079,11 @@ moTest() { # # Returns true (0) if the variable is set, 1 if the variable is unset. moTestVarSet() { - [[ "${!1-a}" == "${!1-b}" ]] + # Don't allow access to outside names + moTestLegal "$1" || return 1 + + [[ "${!1-a}" == "${!1-b}" ]] && return 0 + return 1 } diff --git a/run-tests b/run-tests index b1a0331..01219f5 100755 --- a/run-tests +++ b/run-tests @@ -23,6 +23,9 @@ for TEST in tests/*.expected; do . "${BASE}.env" echo "Do not read this input" | mo "${BASE}.template" fi + ) | ( + cat > "${BASE}.actual"; + cat "${BASE}.actual" ) | diff -U5 - "${TEST}" > "${BASE}.diff" statusCode=$? @@ -34,6 +37,7 @@ for TEST in tests/*.expected; do echo "ok" PASS=$(( PASS + 1 )) rm "${BASE}.diff" + rm "${BASE}.actual" fi done diff --git a/tests/source.expected b/tests/source.expected index a3050ef..83220ad 100644 --- a/tests/source.expected +++ b/tests/source.expected @@ -1,3 +1,4 @@ +Purposely Unsafe for Backwards Compatibility value * 1 * 2 diff --git a/tests/source.sh b/tests/source.sh index 7b35d4d..83b96d8 100755 --- a/tests/source.sh +++ b/tests/source.sh @@ -2,6 +2,7 @@ cd "${0%/*}" || exit 1 cat <&2) 2>&1) +EOF + +echo "${STDOUT[@]}" + +expectedErr="Illegal variable access BASH_VERSINFO" +if [[ ! "$STDERR" =~ "$expectedErr" ]]; then + echo "STDERR should have contained an illegal variable access message:" + echo "$STDERR" +fi + +exit 0