Skip to content

Commit 0e801e1

Browse files
committed
Fix security issues and potential crashes
This new implementation fixes the following issues that we had in the old implementation: - Issues and security risk with paths containing spaces or special parameters - This would not only have made the command fail - But it also created a security issue for parameter injection (e.g. `apk_output_file_path: 'foo --inject this && echo print the rest'`) - Risk of deleting any arbitrary folder on disk (due to use of unchecked `rm -rf`) - Calling the action with `apk_output_file_path: '.'` for example (or a path to any directory) would have the side effect of… deleting the whole content of the current directory before failing - This was even more problematic that this could be an easy genuine accidental error to make—especially if the caller misread the documentation and thought that the command accepted a folder for the output path instead of the path to the apk file. In addition, this commit adds the following features to the action's behavior: - The action will detect if the parent directory of the `apk_output_file_path` does not exist, and exit early with a nice error if so - We now allow `apk_output_file_path` to be a directory, and if so the output file will be put in that directory and use the same basename as the input aab_file_path (but with `.apk` extension instead of `.aab`) - The action now returns the path to the generated file (which is especially useful if you didn't provide an explicit value for `apk_output_file_path` and let the action infer it, or if you provided a directory for this output path)
1 parent 1f43c4c commit 0e801e1

File tree

1 file changed

+60
-40
lines changed

1 file changed

+60
-40
lines changed

lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_generate_apk_from_aab.rb

Lines changed: 60 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,47 @@
11
module Fastlane
22
module Actions
33
class AndroidGenerateApkFromAabAction < Action
4-
def self.generate_command(aab_file_path, apk_output_file_path, keystore_path, keystore_password, keystore_key_alias, signing_key_password)
5-
command = "bundletool build-apks --mode universal --bundle #{aab_file_path} --output-format DIRECTORY --output #{apk_output_file_path} "
6-
code_sign_arguments = "--ks #{keystore_path} --ks-pass #{keystore_password} --ks-key-alias #{keystore_key_alias} --key-pass #{signing_key_password} "
7-
move_and_cleanup_command = "&& mv #{apk_output_file_path}/universal.apk #{apk_output_file_path}_tmp && rm -rf #{apk_output_file_path} && mv #{apk_output_file_path}_tmp #{apk_output_file_path}"
4+
def self.run(params)
5+
begin
6+
sh('command', '-v', 'bundletool', print_command: false, print_command_output: false)
7+
rescue StandardError
8+
UI.user_error!(MISSING_BUNDLETOOL_ERROR_MESSAGE)
9+
end
810

9-
# Attempt to code sign the APK if a keystore_path parameter is specified
10-
command += code_sign_arguments unless keystore_path.nil?
11+
# Parse input parameters
12+
aab_file_path = parse_aab_param(params)
13+
apk_output_file_path = params[:apk_output_file_path] || Pathname(aab_file_path).sub_ext('.apk').to_s
14+
code_sign_arguments = {
15+
'--ks': params[:keystore_path],
16+
'--ks-pass': params[:keystore_password],
17+
'--ks-key-alias': params[:keystore_key_alias],
18+
'--key-pass': params[:signing_key_password]
19+
}.compact.flatten.map(&:to_s)
1120

12-
# Move and rename the universal.apk file to the specified output path and cleanup the directory created by bundletool
13-
command += move_and_cleanup_command
21+
if File.directory?(apk_output_file_path)
22+
apk_output_file_path = File.join(apk_output_file_path, "#{File.basename(aab_file_path, '.aab')}.apk")
23+
end
1424

15-
return command
25+
Dir.mktmpdir('a8c-release-toolkit-bundletool-') do |tmpdir|
26+
sh(
27+
'bundletool', 'build-apks',
28+
'--mode', 'universal',
29+
'--bundle', aab_file_path,
30+
'--output-format', 'DIRECTORY',
31+
'--output', tmpdir,
32+
*code_sign_arguments
33+
)
34+
FileUtils.mkdir_p(File.dirname(apk_output_file_path)) # Create destination directory if it doesn't exist yet
35+
FileUtils.mv(File.join(tmpdir, 'universal.apk'), apk_output_file_path)
36+
end
37+
38+
apk_output_file_path
1639
end
1740

18-
def self.run(params)
19-
begin
20-
sh('command -v bundletool > /dev/null')
21-
rescue StandardError
22-
UI.user_error!('bundletool is not installed. Please install it using the instructions at https://developer.android.com/studio/command-line/bundletool.')
23-
raise
24-
end
41+
#####################################################
2542

43+
def self.parse_aab_param(params)
2644
# If no AAB param was provided, attempt to get it from the lane context
27-
# First GRADLE_ALL_AAB_OUTPUT_PATHS if only one
28-
# Second GRADLE_AAB_OUTPUT_PATH if it is set
29-
# Else use the specified parameter value
3045
if params[:aab_file_path].nil?
3146
all_aab_paths = Actions.lane_context[SharedValues::GRADLE_ALL_AAB_OUTPUT_PATHS] || []
3247
aab_file_path = if all_aab_paths.count == 1
@@ -40,19 +55,17 @@ def self.run(params)
4055

4156
# If no AAB file path was found, raise an error
4257
if aab_file_path.nil?
43-
UI.user_error!('No AAB file path was specified and none was found in the lane context. Please specify the `aab_file_path` parameter or ensure that the relevant build action has been run prior to this action.')
44-
raise
58+
UI.user_error!(NO_AAB_ERROR_MESSAGE)
59+
elsif !File.file?(aab_file_path)
60+
UI.user_error!("The file `#{aab_file_path}` was not found. Please provide a path to an existing file.")
4561
end
4662

47-
apk_output_file_path = params[:apk_output_file_path]
48-
keystore_path = params[:keystore_path]
49-
keystore_password = params[:keystore_password]
50-
keystore_key_alias = params[:keystore_key_alias]
51-
signing_key_password = params[:signing_key_password]
52-
53-
sh(generate_command(aab_file_path, apk_output_file_path, keystore_path, keystore_password, keystore_key_alias, signing_key_password))
63+
aab_file_path
5464
end
5565

66+
MISSING_BUNDLETOOL_ERROR_MESSAGE = 'bundletool is not installed. Please install it using the instructions at https://developer.android.com/studio/command-line/bundletool.'.freeze
67+
NO_AAB_ERROR_MESSAGE = 'No AAB file path was specified and none was found in the lane context. Please specify the `aab_file_path` parameter or ensure that the `gradle` action has been run prior to this action.'.freeze
68+
5669
#####################################################
5770
# @!group Documentation
5871
#####################################################
@@ -62,70 +75,77 @@ def self.description
6275
end
6376

6477
def self.details
65-
'Generates an APK from the specified AAB'
78+
'Generates an APK file from the specified AAB file using `bundletool`'
6679
end
6780

6881
def self.available_options
6982
[
7083
FastlaneCore::ConfigItem.new(
7184
key: :aab_file_path,
7285
env_name: 'ANDROID_AAB_FILE_PATH',
73-
description: 'The path to the AAB file. If not speicified, the action will attempt to read from the lane context using the `SharedValues::GRADLE_ALL_AAB_OUTPUT_PATHS` and `SharedValues::GRADLE_AAB_OUTPUT_PATH` keys',
86+
description: 'The path to the AAB file. If not specified, the action will attempt to read from the lane context using the `SharedValues::GRADLE_ALL_AAB_OUTPUT_PATHS` and `SharedValues::GRADLE_AAB_OUTPUT_PATH` keys',
7487
type: String,
7588
optional: true,
76-
default_value: nil,
77-
verify_block: proc { |p| UI.user_error!("AAB path `#{p}` is not a valid file path.") unless File.file?(p) }
89+
default_value: nil
7890
),
7991
FastlaneCore::ConfigItem.new(
8092
key: :apk_output_file_path,
8193
env_name: 'ANDROID_APK_OUTPUT_PATH',
82-
description: 'The output path where the APK file will be generated. The directory will be created if it does not yet exist',
94+
description: 'The path of the output APK file to generate. If not specified, will use the same path and basename as the `aab_file_path` but with an `.apk` file extension',
8395
type: String,
84-
optional: false,
96+
optional: true,
8597
default_value: nil
8698
),
8799
FastlaneCore::ConfigItem.new(
88100
key: :keystore_path,
89101
env_name: 'ANDROID_KEYSTORE_PATH',
90-
description: 'The path to the keystore file',
102+
description: 'The path to the keystore file (if you want to codesign the APK)',
91103
type: String,
92104
optional: true,
93105
default_value: nil,
94-
verify_block: proc { |p| UI.user_error!("Keystore file path `#{p}` is not a valid file path.") unless File.file?(p) || p.nil }
106+
verify_block: proc { |p| UI.user_error!("Keystore file path `#{p}` is not a valid file path.") unless p.nil? || File.file?(p) }
95107
),
96108
FastlaneCore::ConfigItem.new(
97109
key: :keystore_password,
98110
env_name: 'ANDROID_KEYSTORE_PASSWORD',
99-
description: 'The password for the keystore',
111+
description: 'The password for the keystore (if you want to codesign the APK)',
100112
type: String,
101113
optional: true,
102114
default_value: nil
103115
),
104116
FastlaneCore::ConfigItem.new(
105117
key: :keystore_key_alias,
106118
env_name: 'ANDROID_KEYSTORE_KEY_ALIAS',
107-
description: 'The alias of the key in the keystore',
119+
description: 'The alias of the key in the keystore (if you want to codesign the APK)',
108120
type: String,
109121
optional: true,
110122
default_value: nil
111123
),
112124
FastlaneCore::ConfigItem.new(
113125
key: :signing_key_password,
114126
env_name: 'ANDROID_SIGNING_KEY_PASSWORD',
115-
description: 'The password for the signing key',
127+
description: 'The password for the signing key (if you want to codesign the APK)',
116128
type: String,
117129
optional: true,
118130
default_value: nil
119131
),
120132
]
121133
end
122134

135+
def self.return_type
136+
:string
137+
end
138+
139+
def self.return_value
140+
'The path to the APK that has been generated'
141+
end
142+
123143
def self.authors
124144
['Automattic']
125145
end
126146

127147
def self.is_supported?(platform)
128-
true
148+
platform == 'android'
129149
end
130150
end
131151
end

0 commit comments

Comments
 (0)