Skip to content

Commit

Permalink
Protect certbot-auto against automated downgrades (certbot#6448)
Browse files Browse the repository at this point in the history
With current code, the certbot-auto self-upgrade process can make it actually to downgrade itself, because the comparison done is an equality test between local certbot-auto version and the remote one. This is a flaw for attackers, that could make certbot-auto break itself by falsely advertising it about an old version as the latest one available.

A function is added to make a more advanced comparison between version. Certbot-auto will upgrade itself only if the local version is strictly inferior to the latest one available. For instance, a version 0.28.0 will not upgrade itself if the latest one available on internet is 0.27.1. Similarly, non-official versions like 0.28.0.dev0 will never trigger a self-upgrade, to help development workflows.

This implementation relies only on the Python distribution installed by certbot-auto (supporting 2.7+) and basic shell operations, to be compatible with any UNIX-based system.

* Check version with protection again downgrade

* Create a stable version of letsencrypt-auto to use correctly self-upgrade functionality

* Update letsencrypt-auto-source/letsencrypt-auto.template
  • Loading branch information
adferrand authored and bmw committed Nov 19, 2018
1 parent 4e1c227 commit 78cf8ec
Show file tree
Hide file tree
Showing 3 changed files with 84 additions and 3 deletions.
36 changes: 35 additions & 1 deletion letsencrypt-auto-source/letsencrypt-auto
Original file line number Diff line number Diff line change
Expand Up @@ -912,6 +912,35 @@ OldVenvExists() {
[ -n "$OLD_VENV_PATH" -a -f "$OLD_VENV_PATH/bin/letsencrypt" ]
}

# Given python path, version 1 and version 2, check if version 1 is outdated compared to version 2.
# An unofficial version provided as version 1 (eg. 0.28.0.dev0) will be treated
# specifically by printing "UNOFFICIAL". Otherwise, print "OUTDATED" if version 1
# is outdated, and "UP_TO_DATE" if not.
# This function relies only on installed python environment (2.x or 3.x) by certbot-auto.
CompareVersions() {
"$1" - "$2" "$3" << "UNLIKELY_EOF"
import sys
from distutils.version import StrictVersion
try:
current = StrictVersion(sys.argv[1])
except ValueError:
sys.stdout.write('UNOFFICIAL')
sys.exit()
try:
remote = StrictVersion(sys.argv[2])
except ValueError:
sys.stdout.write('UP_TO_DATE')
sys.exit()
if current < remote:
sys.stdout.write('OUTDATED')
else:
sys.stdout.write('UP_TO_DATE')
UNLIKELY_EOF
}

if [ "$1" = "--le-auto-phase2" ]; then
# Phase 2: Create venv, install LE, and run.

Expand Down Expand Up @@ -1640,7 +1669,12 @@ UNLIKELY_EOF
error "WARNING: couldn't find Python $MIN_PYTHON_VERSION+ to check for updates."
elif ! REMOTE_VERSION=`"$LE_PYTHON" "$TEMP_DIR/fetch.py" --latest-version` ; then
error "WARNING: unable to check for updates."
elif [ "$LE_AUTO_VERSION" != "$REMOTE_VERSION" ]; then
fi

LE_VERSION_STATE=`CompareVersions "$LE_PYTHON" "$LE_AUTO_VERSION" "$REMOTE_VERSION"`
if [ "$LE_VERSION_STATE" = "UNOFFICIAL" ]; then
say "Unofficial certbot-auto version detected, self-upgrade is disabled: $LE_AUTO_VERSION"
elif [ "$LE_VERSION_STATE" = "OUTDATED" ]; then
say "Upgrading certbot-auto $LE_AUTO_VERSION to $REMOTE_VERSION..."

# Now we drop into Python so we don't have to install even more
Expand Down
36 changes: 35 additions & 1 deletion letsencrypt-auto-source/letsencrypt-auto.template
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,35 @@ OldVenvExists() {
[ -n "$OLD_VENV_PATH" -a -f "$OLD_VENV_PATH/bin/letsencrypt" ]
}

# Given python path, version 1 and version 2, check if version 1 is outdated compared to version 2.
# An unofficial version provided as version 1 (eg. 0.28.0.dev0) will be treated
# specifically by printing "UNOFFICIAL". Otherwise, print "OUTDATED" if version 1
# is outdated, and "UP_TO_DATE" if not.
# This function relies only on installed python environment (2.x or 3.x) by certbot-auto.
CompareVersions() {
"$1" - "$2" "$3" << "UNLIKELY_EOF"
import sys
from distutils.version import StrictVersion
try:
current = StrictVersion(sys.argv[1])
except ValueError:
sys.stdout.write('UNOFFICIAL')
sys.exit()
try:
remote = StrictVersion(sys.argv[2])
except ValueError:
sys.stdout.write('UP_TO_DATE')
sys.exit()
if current < remote:
sys.stdout.write('OUTDATED')
else:
sys.stdout.write('UP_TO_DATE')
UNLIKELY_EOF
}

if [ "$1" = "--le-auto-phase2" ]; then
# Phase 2: Create venv, install LE, and run.

Expand Down Expand Up @@ -635,7 +664,12 @@ UNLIKELY_EOF
error "WARNING: couldn't find Python $MIN_PYTHON_VERSION+ to check for updates."
elif ! REMOTE_VERSION=`"$LE_PYTHON" "$TEMP_DIR/fetch.py" --latest-version` ; then
error "WARNING: unable to check for updates."
elif [ "$LE_AUTO_VERSION" != "$REMOTE_VERSION" ]; then
fi

LE_VERSION_STATE=`CompareVersions "$LE_PYTHON" "$LE_AUTO_VERSION" "$REMOTE_VERSION"`
if [ "$LE_VERSION_STATE" = "UNOFFICIAL" ]; then
say "Unofficial certbot-auto version detected, self-upgrade is disabled: $LE_AUTO_VERSION"
elif [ "$LE_VERSION_STATE" = "OUTDATED" ]; then
say "Upgrading certbot-auto $LE_AUTO_VERSION to $REMOTE_VERSION..."

# Now we drop into Python so we don't have to install even more
Expand Down
15 changes: 14 additions & 1 deletion letsencrypt-auto-source/tests/auto_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,19 @@ def tests_dir():
return dirname(abspath(__file__))


def copy_stable(src, dst):
"""
Copy letsencrypt-auto, and replace its current version to its equivalent stable one.
This is needed to test correctly the self-upgrade functionality.
"""
copy(src, dst)
with open(dst, 'r') as file:
filedata = file.read()
filedata = re.sub(r'LE_AUTO_VERSION="(.*)\.dev0"', r'LE_AUTO_VERSION="\1"', filedata)
with open(dst, 'w') as file:
file.write(filedata)


sys.path.insert(0, dirname(tests_dir()))
from build import build as build_le_auto

Expand Down Expand Up @@ -343,7 +356,7 @@ def test_openssl_failure(self):
'v99.9.9/letsencrypt-auto': build_le_auto(version='99.9.9'),
'v99.9.9/letsencrypt-auto.sig': signed('something else')}
with serving(resources) as base_url:
copy(LE_AUTO_PATH, le_auto_path)
copy_stable(LE_AUTO_PATH, le_auto_path)
try:
out, err = run_le_auto(le_auto_path, venv_dir, base_url)
except CalledProcessError as exc:
Expand Down

0 comments on commit 78cf8ec

Please sign in to comment.