mirror of
https://github.com/dokku/dokku.git
synced 2026-05-18 13:15:19 +02:00
fix: prevent tar symlink traversal in archive extraction
Archives passed to git:from-archive and certs:add were extracted without symlink or path validation, allowing a crafted archive to write arbitrary files anywhere writable by the dokku user via symlink traversal. Extraction now pre-scans entries for absolute paths, parent traversal, and unsafe symlinks, applies the GNU tar `--no-unsafe-links` flag when available, and validates symlinks after extraction.
This commit is contained in:
@@ -41,6 +41,9 @@ dokku certs:add node-js-app < cert-key.tar
|
||||
cat yourdomain_com.crt yourdomain_com.ca-bundle > server.crt
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> Archives passed to `certs:add` are validated before extraction to prevent path traversal and symlink escape attacks. Archives containing absolute paths, parent directory traversal entries, or symlinks pointing outside the extraction directory will be rejected.
|
||||
|
||||
#### SSL and Multiple Domains
|
||||
|
||||
When an SSL certificate is associated to an application, the certificate will be associated with _all_ domains currently associated with said application. Your certificate _should_ be associated with all of those domains, otherwise accessing the application will result in SSL errors. If you wish to remove one of the domains from the application, refer to the [domain configuration documentation](/docs/configuration/domains.md).
|
||||
|
||||
@@ -32,3 +32,12 @@ Finally, if the archive url is specified as `--`, the archive will be fetched fr
|
||||
```shell
|
||||
curl -sSL https://github.com/dokku/smoke-test-app/releases/download/2.0.0/smoke-test-app.tar | dokku git:from-archive node-js-app --
|
||||
```
|
||||
|
||||
## Archive Safety
|
||||
|
||||
Archive contents are validated before extraction to prevent path traversal and symlink escape attacks. Archives containing absolute paths, parent directory traversal entries (`..`), or symlinks pointing outside the extraction directory are rejected.
|
||||
|
||||
The following limits can be configured via environment variables:
|
||||
|
||||
- `DOKKU_ARCHIVE_MAX_SIZE` - maximum archive size in bytes (default: `1073741824`, 1 GiB)
|
||||
- `DOKKU_ARCHIVE_MAX_FILES` - maximum number of entries in an archive (default: `10000`)
|
||||
|
||||
@@ -44,7 +44,7 @@ cmd-certs-set() {
|
||||
local CERTS_SET_TMP_WORK_DIR=$(mktemp -d "/tmp/dokku-${DOKKU_PID}-${FUNCNAME[0]}.XXXXXX")
|
||||
pushd "$CERTS_SET_TMP_WORK_DIR" &>/dev/null
|
||||
trap "popd &>/dev/null || true; rm -rf '$CERTS_SET_TMP_WORK_DIR' >/dev/null" RETURN
|
||||
tar xvf - <&0
|
||||
fn-archive-extract-tar-stdin "$CERTS_SET_TMP_WORK_DIR" "certs:add"
|
||||
|
||||
local CRT_FILE_SEARCH=$(find . -not -path '*/\.*' -type f | grep ".crt$")
|
||||
local CRT_FILE_COUNT=$(printf "%s" "$CRT_FILE_SEARCH" | grep -c '^')
|
||||
|
||||
196
plugins/common/archive-functions
Executable file
196
plugins/common/archive-functions
Executable file
@@ -0,0 +1,196 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
[[ $DOKKU_TRACE ]] && set -x
|
||||
|
||||
DOKKU_ARCHIVE_MAX_SIZE="${DOKKU_ARCHIVE_MAX_SIZE:-1073741824}"
|
||||
DOKKU_ARCHIVE_MAX_FILES="${DOKKU_ARCHIVE_MAX_FILES:-10000}"
|
||||
|
||||
fn-archive-log-security-event() {
|
||||
declare desc="logs an archive security event for auditing"
|
||||
declare EVENT="$1" ARCHIVE_TYPE="$2" SOURCE="$3" DETAILS="$4"
|
||||
local TIMESTAMP="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
||||
local USERNAME="${SUDO_USER:-${USER:-unknown}}"
|
||||
echo "[archive-security] timestamp=${TIMESTAMP} user=${USERNAME} event=${EVENT} archive_type=${ARCHIVE_TYPE} source=${SOURCE} details=${DETAILS}" 1>&2
|
||||
}
|
||||
|
||||
fn-archive-tar-flag() {
|
||||
declare desc="returns the appropriate decompression flag for tar based on archive type"
|
||||
declare ARCHIVE_TYPE="$1"
|
||||
case "$ARCHIVE_TYPE" in
|
||||
tar) echo "" ;;
|
||||
tar.gz) echo "-z" ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
fn-archive-check-bomb-protection() {
|
||||
declare desc="validates archive against bomb protection limits"
|
||||
declare ARCHIVE_PATH="$1" ARCHIVE_TYPE="$2"
|
||||
local ARCHIVE_SIZE
|
||||
ARCHIVE_SIZE="$(stat -c %s "$ARCHIVE_PATH" 2>/dev/null || stat -f %z "$ARCHIVE_PATH" 2>/dev/null || echo 0)"
|
||||
|
||||
if [[ "$ARCHIVE_SIZE" -gt "$DOKKU_ARCHIVE_MAX_SIZE" ]]; then
|
||||
fn-archive-log-security-event "rejected_archive_too_large" "$ARCHIVE_TYPE" "$ARCHIVE_PATH" "size=${ARCHIVE_SIZE} max=${DOKKU_ARCHIVE_MAX_SIZE}"
|
||||
dokku_log_fail "Archive exceeds maximum allowed size of ${DOKKU_ARCHIVE_MAX_SIZE} bytes"
|
||||
fi
|
||||
|
||||
local TAR_FLAG
|
||||
TAR_FLAG="$(fn-archive-tar-flag "$ARCHIVE_TYPE")" || return 0
|
||||
|
||||
local FILE_COUNT
|
||||
if [[ -n "$TAR_FLAG" ]]; then
|
||||
FILE_COUNT="$(tar "$TAR_FLAG" -tf "$ARCHIVE_PATH" 2>/dev/null | wc -l)"
|
||||
else
|
||||
FILE_COUNT="$(tar -tf "$ARCHIVE_PATH" 2>/dev/null | wc -l)"
|
||||
fi
|
||||
|
||||
if [[ "$FILE_COUNT" -gt "$DOKKU_ARCHIVE_MAX_FILES" ]]; then
|
||||
fn-archive-log-security-event "rejected_archive_too_many_files" "$ARCHIVE_TYPE" "$ARCHIVE_PATH" "count=${FILE_COUNT} max=${DOKKU_ARCHIVE_MAX_FILES}"
|
||||
dokku_log_fail "Archive contains ${FILE_COUNT} entries which exceeds the maximum of ${DOKKU_ARCHIVE_MAX_FILES}"
|
||||
fi
|
||||
}
|
||||
|
||||
fn-archive-validate-tar-entries() {
|
||||
declare desc="pre-scans a tar archive for dangerous path entries"
|
||||
declare ARCHIVE_PATH="$1" ARCHIVE_TYPE="$2"
|
||||
local TAR_FLAG NAMES VERBOSE
|
||||
TAR_FLAG="$(fn-archive-tar-flag "$ARCHIVE_TYPE")" || dokku_log_fail "Unsupported archive type for validation: ${ARCHIVE_TYPE}"
|
||||
|
||||
if [[ -n "$TAR_FLAG" ]]; then
|
||||
NAMES="$(tar "$TAR_FLAG" -tf "$ARCHIVE_PATH" 2>/dev/null)"
|
||||
VERBOSE="$(tar "$TAR_FLAG" -tvf "$ARCHIVE_PATH" 2>/dev/null)"
|
||||
else
|
||||
NAMES="$(tar -tf "$ARCHIVE_PATH" 2>/dev/null)"
|
||||
VERBOSE="$(tar -tvf "$ARCHIVE_PATH" 2>/dev/null)"
|
||||
fi
|
||||
|
||||
if echo "$NAMES" | grep -qE '^/'; then
|
||||
fn-archive-log-security-event "rejected_absolute_path" "$ARCHIVE_TYPE" "$ARCHIVE_PATH" "absolute path detected"
|
||||
dokku_log_fail "Archive contains entries with absolute paths"
|
||||
fi
|
||||
|
||||
if echo "$NAMES" | grep -qE '(^|/)\.\.(/|$)'; then
|
||||
fn-archive-log-security-event "rejected_path_traversal" "$ARCHIVE_TYPE" "$ARCHIVE_PATH" "parent directory traversal detected"
|
||||
dokku_log_fail "Archive contains entries with parent directory traversal"
|
||||
fi
|
||||
|
||||
while IFS= read -r entry; do
|
||||
[[ -z "$entry" ]] && continue
|
||||
[[ "${entry:0:1}" != "l" ]] && continue
|
||||
local target="${entry##*-> }"
|
||||
case "$target" in
|
||||
/*)
|
||||
fn-archive-log-security-event "rejected_unsafe_symlink" "$ARCHIVE_TYPE" "$ARCHIVE_PATH" "absolute symlink target=${target}"
|
||||
dokku_log_fail "Archive contains symlinks with absolute targets"
|
||||
;;
|
||||
*..*)
|
||||
fn-archive-log-security-event "rejected_unsafe_symlink" "$ARCHIVE_TYPE" "$ARCHIVE_PATH" "traversal symlink target=${target}"
|
||||
dokku_log_fail "Archive contains symlinks pointing outside extraction directory"
|
||||
;;
|
||||
esac
|
||||
done <<<"$VERBOSE"
|
||||
}
|
||||
|
||||
fn-archive-validate-extracted-symlinks() {
|
||||
declare desc="validates that no symlinks in the extraction directory point outside it"
|
||||
declare EXTRACTION_DIR="$1"
|
||||
local CANONICAL_DIR
|
||||
CANONICAL_DIR="$(readlink -f "$EXTRACTION_DIR")"
|
||||
|
||||
local SYMLINKS
|
||||
SYMLINKS="$(find "$EXTRACTION_DIR" -type l 2>/dev/null || true)"
|
||||
[[ -z "$SYMLINKS" ]] && return 0
|
||||
|
||||
while IFS= read -r link; do
|
||||
[[ -z "$link" ]] && continue
|
||||
local target
|
||||
target="$(readlink -f "$link" 2>/dev/null || echo "")"
|
||||
if [[ -z "$target" ]] || [[ "$target" != "$CANONICAL_DIR"* ]]; then
|
||||
fn-archive-log-security-event "rejected_extracted_symlink" "post_extraction" "$link" "target=${target} dir=${CANONICAL_DIR}"
|
||||
dokku_log_fail "Archive contains symlinks pointing outside extraction directory"
|
||||
fi
|
||||
done <<<"$SYMLINKS"
|
||||
}
|
||||
|
||||
fn-archive-tar-supports-no-unsafe-links() {
|
||||
declare desc="returns 0 if the tar binary supports --no-unsafe-links"
|
||||
if tar --help 2>&1 | grep -q -- "--no-unsafe-links"; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
fn-archive-extract-tar() {
|
||||
declare desc="safely extracts a tar archive with symlink protection"
|
||||
declare ARCHIVE_PATH="$1" DEST_DIR="$2" ARCHIVE_TYPE="$3" STRIP_COMPONENTS="${4:-0}"
|
||||
local TAR_FLAG
|
||||
TAR_FLAG="$(fn-archive-tar-flag "$ARCHIVE_TYPE")" || dokku_log_fail "Unsupported archive type for extraction: ${ARCHIVE_TYPE}"
|
||||
|
||||
fn-archive-check-bomb-protection "$ARCHIVE_PATH" "$ARCHIVE_TYPE"
|
||||
fn-archive-validate-tar-entries "$ARCHIVE_PATH" "$ARCHIVE_TYPE"
|
||||
|
||||
fn-archive-log-security-event "extraction_started" "$ARCHIVE_TYPE" "$ARCHIVE_PATH" "dest=${DEST_DIR}"
|
||||
|
||||
local TAR_ARGS=()
|
||||
if fn-archive-tar-supports-no-unsafe-links; then
|
||||
TAR_ARGS+=("--no-unsafe-links")
|
||||
fi
|
||||
if [[ "$STRIP_COMPONENTS" -gt 0 ]]; then
|
||||
TAR_ARGS+=("--strip-components=$STRIP_COMPONENTS")
|
||||
fi
|
||||
|
||||
if [[ -n "$TAR_FLAG" ]] && [[ "${#TAR_ARGS[@]}" -gt 0 ]]; then
|
||||
tar -x "$TAR_FLAG" -C "$DEST_DIR" -f "$ARCHIVE_PATH" "${TAR_ARGS[@]}"
|
||||
elif [[ -n "$TAR_FLAG" ]]; then
|
||||
tar -x "$TAR_FLAG" -C "$DEST_DIR" -f "$ARCHIVE_PATH"
|
||||
elif [[ "${#TAR_ARGS[@]}" -gt 0 ]]; then
|
||||
tar -x -C "$DEST_DIR" -f "$ARCHIVE_PATH" "${TAR_ARGS[@]}"
|
||||
else
|
||||
tar -x -C "$DEST_DIR" -f "$ARCHIVE_PATH"
|
||||
fi
|
||||
|
||||
fn-archive-validate-extracted-symlinks "$DEST_DIR"
|
||||
fn-archive-log-security-event "extraction_completed" "$ARCHIVE_TYPE" "$ARCHIVE_PATH" "dest=${DEST_DIR}"
|
||||
}
|
||||
|
||||
fn-archive-extract-tar-stdin() {
|
||||
declare desc="safely extracts a tar archive from stdin into the current directory"
|
||||
declare DEST_DIR="$1" SOURCE_LABEL="${2:-stdin}"
|
||||
|
||||
local STDIN_TMP
|
||||
STDIN_TMP="$(mktemp "/tmp/dokku-${DOKKU_PID}-archive.XXXXXX")"
|
||||
trap "rm -f '$STDIN_TMP' >/dev/null 2>&1 || true" RETURN
|
||||
|
||||
cat <&0 >"$STDIN_TMP"
|
||||
fn-archive-extract-tar "$STDIN_TMP" "$DEST_DIR" "tar" 0
|
||||
rm -f "$STDIN_TMP" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
fn-archive-extract-zip() {
|
||||
declare desc="safely extracts a zip archive with post-extraction symlink validation"
|
||||
declare ARCHIVE_PATH="$1" DEST_DIR="$2"
|
||||
|
||||
local ARCHIVE_SIZE
|
||||
ARCHIVE_SIZE="$(stat -c %s "$ARCHIVE_PATH" 2>/dev/null || stat -f %z "$ARCHIVE_PATH" 2>/dev/null || echo 0)"
|
||||
if [[ "$ARCHIVE_SIZE" -gt "$DOKKU_ARCHIVE_MAX_SIZE" ]]; then
|
||||
fn-archive-log-security-event "rejected_archive_too_large" "zip" "$ARCHIVE_PATH" "size=${ARCHIVE_SIZE} max=${DOKKU_ARCHIVE_MAX_SIZE}"
|
||||
dokku_log_fail "Archive exceeds maximum allowed size of ${DOKKU_ARCHIVE_MAX_SIZE} bytes"
|
||||
fi
|
||||
|
||||
local FILE_COUNT
|
||||
FILE_COUNT="$(unzip -l "$ARCHIVE_PATH" 2>/dev/null | tail -n 1 | awk '{print $2}')"
|
||||
if [[ -n "$FILE_COUNT" ]] && [[ "$FILE_COUNT" -gt "$DOKKU_ARCHIVE_MAX_FILES" ]]; then
|
||||
fn-archive-log-security-event "rejected_archive_too_many_files" "zip" "$ARCHIVE_PATH" "count=${FILE_COUNT} max=${DOKKU_ARCHIVE_MAX_FILES}"
|
||||
dokku_log_fail "Archive contains ${FILE_COUNT} entries which exceeds the maximum of ${DOKKU_ARCHIVE_MAX_FILES}"
|
||||
fi
|
||||
|
||||
if unzip -l "$ARCHIVE_PATH" 2>/dev/null | awk 'NR>3 {print $NF}' | grep -qE '(^/|(^|/)\.\.(/|$))'; then
|
||||
fn-archive-log-security-event "rejected_unsafe_zip_path" "zip" "$ARCHIVE_PATH" "absolute or traversal path detected"
|
||||
dokku_log_fail "Archive contains entries with unsafe paths"
|
||||
fi
|
||||
|
||||
fn-archive-log-security-event "extraction_started" "zip" "$ARCHIVE_PATH" "dest=${DEST_DIR}"
|
||||
unzip -d "$DEST_DIR" "$ARCHIVE_PATH"
|
||||
fn-archive-validate-extracted-symlinks "$DEST_DIR"
|
||||
fn-archive-log-security-event "extraction_completed" "zip" "$ARCHIVE_PATH" "dest=${DEST_DIR}"
|
||||
}
|
||||
@@ -2,6 +2,10 @@
|
||||
set -eo pipefail
|
||||
[[ $DOKKU_TRACE ]] && set -x
|
||||
|
||||
if [[ -f "${PLUGIN_CORE_AVAILABLE_PATH:-/var/lib/dokku/core-plugins/available}/common/archive-functions" ]]; then
|
||||
source "${PLUGIN_CORE_AVAILABLE_PATH:-/var/lib/dokku/core-plugins/available}/common/archive-functions"
|
||||
fi
|
||||
|
||||
has_tty() {
|
||||
declare desc="return 0 if we have a tty"
|
||||
if [[ "$DOKKU_FORCE_TTY" == "true" ]]; then
|
||||
|
||||
@@ -26,13 +26,13 @@ trigger-git-git-from-archive() {
|
||||
local COMMON_PREFIX=$(tar -tf "$TMP_WORK_DIR_2/src.tar" | sed -e 'N;s/^\(.*\).*\n\1.*$/\1\n\1/;D')
|
||||
local BOGUS_PARTS=$(echo "$COMMON_PREFIX " | awk 'BEGIN{FS="/"} {print NF-1}')
|
||||
dokku_log_verbose "Striping $BOGUS_PARTS worth of directories from tarball"
|
||||
tar -x -C "$TMP_WORK_DIR_3" -f "$TMP_WORK_DIR_2/src.tar" --strip-components="$BOGUS_PARTS"
|
||||
fn-archive-extract-tar "$TMP_WORK_DIR_2/src.tar" "$TMP_WORK_DIR_3" "tar" "$BOGUS_PARTS"
|
||||
elif [[ "$ARCHIVE_TYPE" == "tar.gz" ]]; then
|
||||
dokku_log_verbose "Extracting gzipped tarball"
|
||||
tar -x -C "$TMP_WORK_DIR_3" -f "$TMP_WORK_DIR_2/src.tar.gz" -z
|
||||
fn-archive-extract-tar "$TMP_WORK_DIR_2/src.tar.gz" "$TMP_WORK_DIR_3" "tar.gz" 0
|
||||
elif [[ "$ARCHIVE_TYPE" == "zip" ]]; then
|
||||
dokku_log_verbose "Extracting zipball"
|
||||
unzip -d "$TMP_WORK_DIR_3" "$TMP_WORK_DIR_2/src.zip"
|
||||
fn-archive-extract-zip "$TMP_WORK_DIR_2/src.zip" "$TMP_WORK_DIR_3"
|
||||
fi
|
||||
|
||||
chmod -R u+r "$TMP_WORK_DIR_3"
|
||||
|
||||
178
tests/unit/archive-security.bats
Normal file
178
tests/unit/archive-security.bats
Normal file
@@ -0,0 +1,178 @@
|
||||
#!/usr/bin/env bats
|
||||
|
||||
load test_helper
|
||||
|
||||
ARCHIVE_TMP_DIR="${BATS_TMPDIR}/archive-security"
|
||||
|
||||
setup() {
|
||||
global_setup
|
||||
create_app
|
||||
mkdir -p "$ARCHIVE_TMP_DIR"
|
||||
}
|
||||
|
||||
teardown() {
|
||||
rm -rf "$ARCHIVE_TMP_DIR"
|
||||
rm -f /tmp/dokku-archive-security-canary.txt
|
||||
destroy_app
|
||||
global_teardown
|
||||
}
|
||||
|
||||
create_absolute_symlink_tar() {
|
||||
local OUTPUT="$1" FORMAT="${2:-tar}"
|
||||
python3 - "$OUTPUT" "$FORMAT" <<'PY'
|
||||
import io, sys, tarfile
|
||||
output, fmt = sys.argv[1], sys.argv[2]
|
||||
mode = "w:gz" if fmt == "tar.gz" else "w"
|
||||
with tarfile.open(output, mode) as t:
|
||||
link = tarfile.TarInfo("pwn")
|
||||
link.type = tarfile.SYMTYPE
|
||||
link.linkname = "/tmp"
|
||||
t.addfile(link)
|
||||
payload = b"canary content\n"
|
||||
fi = tarfile.TarInfo("pwn/dokku-archive-security-canary.txt")
|
||||
fi.size = len(payload)
|
||||
t.addfile(fi, io.BytesIO(payload))
|
||||
readme = b"# dummy\n"
|
||||
ri = tarfile.TarInfo("README.md")
|
||||
ri.size = len(readme)
|
||||
t.addfile(ri, io.BytesIO(readme))
|
||||
PY
|
||||
}
|
||||
|
||||
create_relative_traversal_symlink_tar() {
|
||||
local OUTPUT="$1" FORMAT="${2:-tar}"
|
||||
python3 - "$OUTPUT" "$FORMAT" <<'PY'
|
||||
import io, sys, tarfile
|
||||
output, fmt = sys.argv[1], sys.argv[2]
|
||||
mode = "w:gz" if fmt == "tar.gz" else "w"
|
||||
with tarfile.open(output, mode) as t:
|
||||
link = tarfile.TarInfo("pwn")
|
||||
link.type = tarfile.SYMTYPE
|
||||
link.linkname = "../../../../tmp"
|
||||
t.addfile(link)
|
||||
payload = b"canary content\n"
|
||||
fi = tarfile.TarInfo("pwn/dokku-archive-security-canary.txt")
|
||||
fi.size = len(payload)
|
||||
t.addfile(fi, io.BytesIO(payload))
|
||||
PY
|
||||
}
|
||||
|
||||
create_absolute_path_tar() {
|
||||
local OUTPUT="$1" FORMAT="${2:-tar}"
|
||||
python3 - "$OUTPUT" "$FORMAT" <<'PY'
|
||||
import io, sys, tarfile
|
||||
output, fmt = sys.argv[1], sys.argv[2]
|
||||
mode = "w:gz" if fmt == "tar.gz" else "w"
|
||||
with tarfile.open(output, mode) as t:
|
||||
payload = b"absolute path payload\n"
|
||||
fi = tarfile.TarInfo("/tmp/dokku-archive-security-canary.txt")
|
||||
fi.size = len(payload)
|
||||
t.addfile(fi, io.BytesIO(payload))
|
||||
PY
|
||||
}
|
||||
|
||||
create_traversal_path_tar() {
|
||||
local OUTPUT="$1" FORMAT="${2:-tar}"
|
||||
python3 - "$OUTPUT" "$FORMAT" <<'PY'
|
||||
import io, sys, tarfile
|
||||
output, fmt = sys.argv[1], sys.argv[2]
|
||||
mode = "w:gz" if fmt == "tar.gz" else "w"
|
||||
with tarfile.open(output, mode) as t:
|
||||
payload = b"traversal payload\n"
|
||||
fi = tarfile.TarInfo("../../../tmp/dokku-archive-security-canary.txt")
|
||||
fi.size = len(payload)
|
||||
t.addfile(fi, io.BytesIO(payload))
|
||||
PY
|
||||
}
|
||||
|
||||
create_evil_certs_tar() {
|
||||
local OUTPUT="$1"
|
||||
python3 - "$OUTPUT" <<'PY'
|
||||
import io, sys, tarfile
|
||||
output = sys.argv[1]
|
||||
with tarfile.open(output, "w") as t:
|
||||
link = tarfile.TarInfo("pwn")
|
||||
link.type = tarfile.SYMTYPE
|
||||
link.linkname = "/tmp"
|
||||
t.addfile(link)
|
||||
payload = b"canary content\n"
|
||||
fi = tarfile.TarInfo("pwn/dokku-archive-security-canary.txt")
|
||||
fi.size = len(payload)
|
||||
t.addfile(fi, io.BytesIO(payload))
|
||||
crt = b"-----BEGIN CERTIFICATE-----\nfake\n-----END CERTIFICATE-----\n"
|
||||
key = b"-----BEGIN PRIVATE KEY-----\nfake\n-----END PRIVATE KEY-----\n"
|
||||
tcrt = tarfile.TarInfo("server.crt")
|
||||
tcrt.size = len(crt)
|
||||
t.addfile(tcrt, io.BytesIO(crt))
|
||||
tkey = tarfile.TarInfo("server.key")
|
||||
tkey.size = len(key)
|
||||
t.addfile(tkey, io.BytesIO(key))
|
||||
PY
|
||||
}
|
||||
|
||||
@test "(archive-security) git:from-archive rejects tar with absolute symlink target" {
|
||||
create_absolute_symlink_tar "$ARCHIVE_TMP_DIR/evil.tar" "tar"
|
||||
run /bin/bash -c "cat $ARCHIVE_TMP_DIR/evil.tar | dokku git:from-archive $TEST_APP --"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_failure
|
||||
assert_output_contains "absolute targets"
|
||||
[[ ! -f /tmp/dokku-archive-security-canary.txt ]]
|
||||
}
|
||||
|
||||
@test "(archive-security) git:from-archive rejects tar.gz with absolute symlink target" {
|
||||
create_absolute_symlink_tar "$ARCHIVE_TMP_DIR/evil.tar.gz" "tar.gz"
|
||||
run /bin/bash -c "cat $ARCHIVE_TMP_DIR/evil.tar.gz | dokku git:from-archive --archive-type tar.gz $TEST_APP --"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_failure
|
||||
assert_output_contains "absolute targets"
|
||||
[[ ! -f /tmp/dokku-archive-security-canary.txt ]]
|
||||
}
|
||||
|
||||
@test "(archive-security) git:from-archive rejects tar with relative traversal symlink" {
|
||||
create_relative_traversal_symlink_tar "$ARCHIVE_TMP_DIR/evil.tar" "tar"
|
||||
run /bin/bash -c "cat $ARCHIVE_TMP_DIR/evil.tar | dokku git:from-archive $TEST_APP --"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_failure
|
||||
assert_output_contains "symlinks pointing outside extraction directory"
|
||||
[[ ! -f /tmp/dokku-archive-security-canary.txt ]]
|
||||
}
|
||||
|
||||
@test "(archive-security) git:from-archive rejects tar with absolute paths" {
|
||||
create_absolute_path_tar "$ARCHIVE_TMP_DIR/evil.tar" "tar"
|
||||
run /bin/bash -c "cat $ARCHIVE_TMP_DIR/evil.tar | dokku git:from-archive $TEST_APP --"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_failure
|
||||
assert_output_contains "absolute paths"
|
||||
[[ ! -f /tmp/dokku-archive-security-canary.txt ]]
|
||||
}
|
||||
|
||||
@test "(archive-security) git:from-archive rejects tar with parent traversal" {
|
||||
create_traversal_path_tar "$ARCHIVE_TMP_DIR/evil.tar" "tar"
|
||||
run /bin/bash -c "cat $ARCHIVE_TMP_DIR/evil.tar | dokku git:from-archive $TEST_APP --"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_failure
|
||||
assert_output_contains "Archive contains entries with parent directory traversal"
|
||||
[[ ! -f /tmp/dokku-archive-security-canary.txt ]]
|
||||
}
|
||||
|
||||
@test "(archive-security) certs:add rejects tar with absolute symlink target" {
|
||||
create_evil_certs_tar "$ARCHIVE_TMP_DIR/evil-certs.tar"
|
||||
run /bin/bash -c "cat $ARCHIVE_TMP_DIR/evil-certs.tar | dokku certs:add $TEST_APP"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_failure
|
||||
assert_output_contains "absolute targets"
|
||||
[[ ! -f /tmp/dokku-archive-security-canary.txt ]]
|
||||
}
|
||||
|
||||
@test "(archive-security) certs:add still works with legitimate tarball" {
|
||||
run /bin/bash -c "dokku certs:add $TEST_APP < $BATS_TEST_DIRNAME/server_ssl.tar"
|
||||
echo "output: $output"
|
||||
echo "status: $status"
|
||||
assert_success
|
||||
}
|
||||
Reference in New Issue
Block a user