Skip to content

Commit 1863c50

Browse files
authored
fix(github-action)!: resolve command injection vulnerability in action script (#56)
Prevents the malicious execution of arbitrary code when a command injection is defined in the action yaml as part of the action parameter specification. Low impact exploitation if proper least privilege is implemented and pull request code is reviewed before merging to a release branch. BREAKING CHANGE: The `root_options` action input parameter has been removed because it created a command injection vulnerability for arbitrary code to execute within the container context of the GitHub action if a command injection code was provided as part of the `root_options` parameter string. To eliminate the vulnerability, each relevant option that can be provided to `semantic-release` has been individually added as its own parameter and will be processed individually to prevent command injection. Please review our Github Actions Configuration page on the Python Semantic Release Documentation website to review the newly available configuration options that replace the `root_options` parameter. Resolves: #55 * test: fix current tests to match new parameters * test: add test for loading custom configuration
1 parent 799ce25 commit 1863c50

File tree

7 files changed

+156
-19
lines changed

7 files changed

+156
-19
lines changed

action.yml

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@ branding:
88
color: orange
99

1010
inputs:
11+
12+
config_file:
13+
description: |
14+
Path to a custom semantic-release configuration file. By default, an empty
15+
string will look for a pyproject.toml file in the current directory. This is the same
16+
as passing the `-c` or `--config` parameter to semantic-release.
17+
default: ""
18+
required: false
19+
1120
directory:
1221
description: |
1322
Sub-directory to change into before running semantic-release publish
@@ -20,12 +29,13 @@ inputs:
2029
edit releases.
2130
required: true
2231

23-
root_options:
32+
no_operation_mode:
2433
description: |
25-
Additional options for the root `semantic-release` command.
26-
Example: -vv --noop
34+
If set to true, the github action will pass the `--noop` parameter to
35+
semantic-release. This will cause semantic-release to run in "no operation"
36+
mode. See the documentation for more information on this parameter.
37+
default: "false"
2738
required: false
28-
default: "-v"
2939

3040
tag:
3141
description: |
@@ -34,6 +44,14 @@ inputs:
3444
will be identified by Python Semantic Release and used to publish to.
3545
required: false
3646

47+
verbosity:
48+
description: |
49+
Set the verbosity level of the output as the number of -v's to pass to
50+
semantic-release. 0 is no extra output, 1 is info level output, 2 is
51+
debug output, and 3 is silly debug level of output.
52+
default: "1"
53+
required: false
54+
3755
runs:
3856
using: docker
3957
image: src/Dockerfile

src/action.sh

Lines changed: 71 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,87 @@ explicit_run_cmd() {
88
eval "$cmd"
99
}
1010

11+
# Convert "true"/"false" into command line args, returns "" if not defined
12+
eval_boolean_action_input() {
13+
local -r input_name="$1"
14+
shift
15+
local -r flag_value="$1"
16+
shift
17+
local -r if_true="$1"
18+
shift
19+
local -r if_false="$1"
20+
21+
if [ -z "$flag_value" ]; then
22+
printf ""
23+
elif [ "$flag_value" = "true" ]; then
24+
printf '%s\n' "$if_true"
25+
elif [ "$flag_value" = "false" ]; then
26+
printf '%s\n' "$if_false"
27+
else
28+
printf 'Error: Invalid value for input %s: %s is not "true" or "false\n"' \
29+
"$input_name" "$flag_value" >&2
30+
return 1
31+
fi
32+
}
33+
34+
# Convert string input into command line args, returns "" if undefined
35+
eval_string_input() {
36+
local -r input_name="$1"
37+
shift
38+
local -r if_defined="$1"
39+
shift
40+
local value
41+
value="$(printf '%s' "$1" | tr -d ' ')"
42+
43+
if [ -z "$value" ]; then
44+
printf ""
45+
return 0
46+
fi
47+
48+
printf '%s' "${if_defined/\%s/$value}"
49+
}
50+
1151
# See https://github.com/actions/runner-images/issues/6775#issuecomment-1409268124
1252
# and https://github.com/actions/runner-images/issues/6775#issuecomment-1410270956
1353
git config --system --add safe.directory "*"
1454

1555
# Change to configured directory
16-
cd "${INPUT_DIRECTORY}"
56+
cd "${INPUT_DIRECTORY}" || exit 1
1757

1858
# Make Token available as a correctly-named environment variables
1959
export GH_TOKEN="${INPUT_GITHUB_TOKEN}"
2060

21-
# Bash array to store publish arguments
22-
PUBLISH_ARGS=()
61+
# Bash array to store semantic release root options
62+
ROOT_OPTIONS=()
2363

24-
# Add publish arguments as necessary
25-
if [ -n "${INPUT_TAG}" ]; then
26-
PUBLISH_ARGS+=("--tag ${INPUT_TAG}")
64+
if ! printf '%s\n' "$INPUT_VERBOSITY" | grep -qE '^[0-9]+$'; then
65+
printf "Error: Input 'verbosity' must be a positive integer\n" >&2
66+
exit 1
2767
fi
2868

69+
VERBOSITY_OPTIONS=""
70+
for ((i = 0; i < INPUT_VERBOSITY; i++)); do
71+
[ "$i" -eq 0 ] && VERBOSITY_OPTIONS="-"
72+
VERBOSITY_OPTIONS+="v"
73+
done
74+
75+
ROOT_OPTIONS+=("$VERBOSITY_OPTIONS")
76+
77+
if [ -n "$INPUT_CONFIG_FILE" ]; then
78+
# Check if the file exists
79+
if [ ! -f "$INPUT_CONFIG_FILE" ]; then
80+
printf "Error: Input 'config_file' does not exist: %s\n" "$INPUT_CONFIG_FILE" >&2
81+
exit 1
82+
fi
83+
84+
ROOT_OPTIONS+=("$(eval_string_input "config_file" "--config %s" "$INPUT_CONFIG_FILE")") || exit 1
85+
fi
86+
87+
ROOT_OPTIONS+=("$(eval_boolean_action_input "no_operation_mode" "$INPUT_NO_OPERATION_MODE" "--noop" "")") || exit 1
88+
89+
# Bash array to store publish arguments
90+
PUBLISH_ARGS=()
91+
PUBLISH_ARGS+=("$(eval_string_input "tag" "--tag %s" "$INPUT_TAG")") || exit 1
92+
2993
# Run Semantic Release
30-
explicit_run_cmd "$PSR_VENV_BIN/semantic-release ${INPUT_ROOT_OPTIONS} publish ${PUBLISH_ARGS[*]}"
94+
explicit_run_cmd "$PSR_VENV_BIN/semantic-release ${ROOT_OPTIONS[*]} publish ${PUBLISH_ARGS[*]}"
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[semantic-release]
2+
commit_parser = "emoji"
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#!/bin/bash
2+
3+
__file__="$(realpath "${BASH_SOURCE[0]}")"
4+
__directory__="$(dirname "${__file__}")"
5+
6+
if ! [ "${UTILS_LOADED}" = "true" ]; then
7+
# shellcheck source=tests/utils.sh
8+
source "$__directory__/../utils.sh"
9+
fi
10+
11+
test_with_custom_config() {
12+
# Using default configuration within PSR with no modifications
13+
# triggering the NOOP mode to prevent errors since the repo doesn't exist
14+
# We are just trying to test that the root options are passed to the action
15+
# without a fatal error
16+
local index="${1:?Index not provided}"
17+
local test_name="${FUNCNAME[0]}"
18+
19+
# Create expectations & set env variables that will be passed in for Docker command
20+
local WITH_VAR_GITHUB_TOKEN="ghp_1x2x3x4x5x6x7x8x9x0x1x2x3x4x5x6x7x8x9x0"
21+
local WITH_VAR_NO_OPERATION_MODE="true"
22+
local WITH_VAR_CONFIG_FILE="releaserc.toml"
23+
local expected_psr_cmd=".*/bin/semantic-release -v --config releaserc.toml --noop publish"
24+
25+
# Execute the test & capture output
26+
# Fatal errors if exit code is not 0
27+
local output=""
28+
if ! output="$(run_test "$index. $test_name" 2>&1)"; then
29+
# Log the output for debugging purposes
30+
log "$output"
31+
error "fatal error occurred!"
32+
error "::error:: $test_name failed!"
33+
return 1
34+
fi
35+
36+
# Evaluate the output to ensure the expected command is present
37+
if ! printf '%s' "$output" | grep -q "$expected_psr_cmd"; then
38+
# Log the output for debugging purposes
39+
log "$output"
40+
error "Failed to find the expected command in the output!"
41+
error "\tExpected Command: $expected_psr_cmd"
42+
error "::error:: $test_name failed!"
43+
return 1
44+
fi
45+
46+
log "\n$index. $test_name: PASSED!"
47+
}

tests/suite/test_publish_w_tag.sh

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,14 @@ test_with_tag() {
1414
# We are just trying to test that the root options & tag arguments are
1515
# passed to the action without a fatal error
1616
local index="${1:?Index not provided}"
17-
local test_name="Test with tag"
17+
local test_name="${FUNCNAME[0]}"
1818

1919
# Create expectations & set env variables that will be passed in for Docker command
2020
local WITH_VAR_GITHUB_TOKEN="ghp_1x2x3x4x5x6x7x8x9x0x1x2x3x4x5x6x7x8x9x0"
2121
local WITH_VAR_TAG="v1.0.0"
22-
local WITH_VAR_ROOT_OPTIONS="--noop -vv"
23-
local expected_psr_cmd=".*/bin/semantic-release $WITH_VAR_ROOT_OPTIONS publish --tag $WITH_VAR_TAG"
22+
local WITH_VAR_NO_OPERATION_MODE="true"
23+
local WITH_VAR_VERBOSITY="2"
24+
local expected_psr_cmd=".*/bin/semantic-release -vv --noop publish --tag $WITH_VAR_TAG"
2425

2526
# Execute the test & capture output
2627
# Fatal errors if exit code is not 0

tests/suite/test_publish_wo_tag.sh

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,13 @@ test_without_tag() {
1414
# We are just trying to test that the root options are passed to the action
1515
# without a fatal error
1616
local index="${1:?Index not provided}"
17-
local test_name="Test without tag"
17+
local test_name="${FUNCNAME[0]}"
1818

1919
# Create expectations & set env variables that will be passed in for Docker command
2020
local WITH_VAR_GITHUB_TOKEN="ghp_1x2x3x4x5x6x7x8x9x0x1x2x3x4x5x6x7x8x9x0"
21-
local WITH_VAR_ROOT_OPTIONS="--noop -vv"
22-
local expected_psr_cmd=".*/bin/semantic-release $WITH_VAR_ROOT_OPTIONS publish"
21+
local WITH_VAR_NO_OPERATION_MODE="true"
22+
local WITH_VAR_VERBOSITY="1"
23+
local expected_psr_cmd=".*/bin/semantic-release -v --noop publish"
2324

2425
# Execute the test & capture output
2526
# Fatal errors if exit code is not 0

tests/utils.sh

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,15 @@ explicit_run_cmd() {
2222
}
2323

2424
run_test() {
25-
local test_name="$1"
25+
local test_name="${1:?Test name not provided}"
26+
test_name="${test_name//_/ }"
27+
test_name="$(tr "[:lower:]" "[:upper:]" <<< "${test_name:0:1}")${test_name:1}"
2628

2729
# Set Defaults based on action.yml
2830
[ -z "$WITH_VAR_DIRECTORY" ] && local WITH_VAR_DIRECTORY="."
29-
[ -z "$WITH_VAR_ROOT_OPTIONS" ] && local WITH_VAR_ROOT_OPTIONS="-v"
31+
[ -z "$WITH_VAR_CONFIG_FILE" ] && local WITH_VAR_CONFIG_FILE=""
32+
[ -z "$WITH_VAR_NO_OPERATION_MODE" ] && local WITH_VAR_NO_OPERATION_MODE="false"
33+
[ -z "$WITH_VAR_VERBOSITY" ] && local WITH_VAR_VERBOSITY="1"
3034

3135
# Extract all WITH_VAR_ variables dynamically from environment
3236
local ENV_ARGS=()

0 commit comments

Comments
 (0)