Skip to content

Commit f921179

Browse files
authored
{confcom}: Simplify logic in map_image_from_tar(_compatibility)?, new integrity-vhd (Azure#9838)
* {confcom}: Simplify logic in map_image_from_tar(_compatibility)? The current code extracts an item in the tar based on a path controlled by the manifest within the tar, which is prone to path traversal on Linux and Windows, and potentially other path confusion attacks on Windows if the Config field in the manifest contains special names. This extraction is unnecessary as we can read the content of the config directly from the tar file using extractfile. This commit simplifies it to do that. Also, there are two versions of the map_image_from_tar function, with the second one introduced in Azure#7414. Later code changes in Azure#8238 means that these functions now do basically the same thing (with different clean up code). After this simplification, these two functions are exactly the same, so let's also remove the _compatibility one. Before: > az confcom acipolicygen --image mcr.microsoft.com/aci/skr:2.14 --tar image-docker-malformed.tar --outraw-pretty-print ... Pulling and hashing images...: 0%| | 0/2 [00:00<?, ?percent/s] ERROR: The command failed with an unexpected error. Here is the traceback: ERROR: [Errno 13] Permission denied: '../../../../../blobs' Traceback (most recent call last): File "/home/mao/.az-cli-newpy.venv/lib/python3.13/site-packages/knack/cli.py", line 233, in invoke cmd_result = self.invocation.execute(args) File "/home/mao/src/github.com/Microsoft/azure-cli/src/azure-cli-core/azure/cli/core/commands/__init__.py", line 677, in execute raise ex File "/home/mao/src/github.com/Microsoft/azure-cli/src/azure-cli-core/azure/cli/core/commands/__init__.py", line 820, in _run_jobs_serially results.append(self._run_job(expanded_arg, cmd_copy)) ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/mao/src/github.com/Microsoft/azure-cli/src/azure-cli-core/azure/cli/core/commands/__init__.py", line 789, in _run_job result = cmd_copy(params) File "/home/mao/src/github.com/Microsoft/azure-cli/src/azure-cli-core/azure/cli/core/commands/__init__.py", line 335, in __call__ return self.handler(*args, **kwargs) ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^ File "/home/mao/src/github.com/Microsoft/azure-cli/src/azure-cli-core/azure/cli/core/commands/command_operation.py", line 120, in handler return op(**command_args) File "/home/mao/.azure/cliextensions/confcom/azext_confcom/custom.py", line 197, in acipolicygen_confcom policy.populate_policy_content_for_all_images( ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^ individual_image=bool(image_name), tar_mapping=tar_mapping, faster_hashing=faster_hashing ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ) ^ File "/home/mao/.azure/cliextensions/confcom/azext_confcom/security_policy.py", line 485, in populate_policy_content_for_all_images image_info, tar = get_image_info(progress, message_queue, tar_mapping, image) ~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/mao/.azure/cliextensions/confcom/azext_confcom/template_util.py", line 111, in get_image_info image_info = os_util.map_image_from_tar_backwards_compatibility( image_name, tar_file, tar_location ) File "/home/mao/.azure/cliextensions/confcom/azext_confcom/os_util.py", line 190, in map_image_from_tar_backwards_compatibility tar.extract(info_file.name, path=tar_dir) ~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/mao/.local/share/uv/python/cpython-3.13.12-linux-x86_64-gnu/lib/python3.13/tarfile.py", line 2416, in extract self._extract_one(tarinfo, path, set_attrs, numeric_owner) ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/mao/.local/share/uv/python/cpython-3.13.12-linux-x86_64-gnu/lib/python3.13/tarfile.py", line 2464, in _extract_one self._handle_fatal_error(e) ~~~~~~~~~~~~~~~~~~~~~~~~^^^ File "/home/mao/.local/share/uv/python/cpython-3.13.12-linux-x86_64-gnu/lib/python3.13/tarfile.py", line 2458, in _extract_one self._extract_member(tarinfo, os.path.join(path, tarinfo.name), ~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ set_attrs=set_attrs, ^^^^^^^^^^^^^^^^^^^^ numeric_owner=numeric_owner, ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ filter_function=filter_function, ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ extraction_root=path) ^^^^^^^^^^^^^^^^^^^^^ File "/home/mao/.local/share/uv/python/cpython-3.13.12-linux-x86_64-gnu/lib/python3.13/tarfile.py", line 2539, in _extract_member os.makedirs(upperdirs, exist_ok=True) ~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^ File "<frozen os>", line 218, in makedirs File "<frozen os>", line 228, in makedirs PermissionError: [Errno 13] Permission denied: '../../../../../blobs' To check existing issues, please visit: https://github.com/Azure/azure-cli/issues After: > az confcom acipolicygen --image mcr.microsoft.com/aci/skr:2.14 --tar image-docker-malformed.tar --outraw-pretty-print | grep -C10 layers ... Pulling and hashing images...: 100%|████████████████████████████████████████████████████████████████████████████████████████████████| 2/2 [00:01<00:00, 1.38percent/s] WARNING: mcr.microsoft.com/aci/skr:2.14 read from local tar file "strategy": "re2" }, { "pattern": "azurecontainerinstance_restarted_by=.+", "required": false, "strategy": "re2" } ], "exec_processes": [], "id": "mcr.microsoft.com/aci/skr:2.14", "layers": [ "a189b02d4858578459fda1dfbd7c6a4557c44208b9829e02b931771a6d611c39", "300f9661fb3d46c0f299ad6f552b7ad0c41ea5141755b0b3feaca3081a108f7a", "0afffca98bacf8e7b6e6f7982459a03219f60555523163c73c4b092e0a3deef2", "eefefd5009aed4ba4478876995d1a18aa3a670661fcc61d2e4cba6e2b79da0a1", "b868a7e1bebef40e5bf4d58fe271c0a10a351e68b12179ec019af9f6c75781ae", "8b4842f06982817534a75bcf71865213b09dfa8313229c384e5201dadbd75e25" ], ... (Validated that this matches the result of using the image reference directly without a tar, and also matches with the result of using the oci tar from `oras backup`) Signed-off-by: Tingmao Wang <tingmaowang@microsoft.com> * {confcom} Bump version to 2.0.0b3 * {confcom}: Fix type on load_json_from_str and read_file_from_tar * {confcom}: Release 2.0.0 (non-preview) * {confcom}: Bump integrity-vhd to v2.1 Take Windows version check for C-WCOW policy generation --------- Signed-off-by: Tingmao Wang <tingmaowang@microsoft.com>
1 parent 5316e0e commit f921179

6 files changed

Lines changed: 35 additions & 82 deletions

File tree

src/confcom/HISTORY.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22
33
Release History
44
===============
5+
6+
2.0.0
7+
+++++
8+
* Fix path traversal when generating policies from untrusted image tar files
9+
510
2.0.0b2
611
+++++++
712
* Fix default working directory for Windows containers being set to C:\\ if the image doesn't specify one.
Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
{
2-
"azext.minCliCoreVersion": "2.26.2",
3-
"azext.isPreview": true
2+
"azext.minCliCoreVersion": "2.26.2"
43
}

src/confcom/azext_confcom/os_util.py

Lines changed: 24 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def clean_up_temp_folder(temp_file_path: str) -> None:
4646
shutil.rmtree(folder_name)
4747

4848

49-
def load_json_from_str(data: str) -> dict:
49+
def load_json_from_str(data: str | bytes | bytearray) -> dict:
5050
if data:
5151
try:
5252
return json.loads(data)
@@ -159,56 +159,13 @@ def load_tar_mapping_from_config_file(path: str) -> dict:
159159
return output_dict
160160

161161

162-
def map_image_from_tar_backwards_compatibility(image_name: str, tar: TarFile, tar_location: str):
163-
tar_dir = os.path.dirname(tar_location)
164-
# grab all files in the folder and only take the one that's named with hex values and a json extension
165-
members = tar.getmembers()
166-
167-
info_file = None
168-
# if there's more than one image in the tarball, we need to do some more logic
169-
if len(members) > 0:
170-
# extract just the manifest file and see if any of the RepoTags match the image_name we're searching for
171-
# the manifest.json should have a list of all the image tags
172-
# and what json files they map to to get env vars, startup cmd, etc.
173-
tar.extract("manifest.json", path=tar_dir)
174-
manifest_path = os.path.join(tar_dir, "manifest.json")
175-
manifest = load_json_from_file(manifest_path)
176-
# if we match a RepoTag to the image, stop searching
177-
for image in manifest:
178-
if image_name in image.get("RepoTags"):
179-
info_file = [
180-
item for item in members if item.name == image.get("Config")
181-
][0]
182-
break
183-
# remove the extracted manifest file to clean up
184-
force_delete_silently(manifest_path)
185-
else:
186-
eprint(f"Tarball at {tar_location} contains no images")
187-
188-
if not info_file:
189-
return None
190-
tar.extract(info_file.name, path=tar_dir)
191-
192-
# get the path of the json file and read it in
193-
image_info_file_path = os.path.join(tar_dir, info_file.name)
194-
image_info_raw = load_json_from_file(image_info_file_path)
195-
# delete the extracted json file to clean up
196-
force_delete_silently(image_info_file_path)
197-
image_info = image_info_raw.get("config")
198-
# importing the constant from config.py gives a circular dependency error
199-
image_info["Architecture"] = image_info_raw.get("architecture")
200-
201-
shutil.rmtree("blobs", ignore_errors=True)
202-
return image_info
203-
204-
205162
def get_oci_image_name(image_name: str) -> str:
206163
if "/" not in image_name:
207164
return f"docker.io/library/{image_name}"
208165
return image_name
209166

210167

211-
def read_file_from_tar(tar: TarFile, filename: str) -> str:
168+
def read_file_from_tar(tar: TarFile, filename: str) -> bytes:
212169
try:
213170
return tar.extractfile(filename).read()
214171
except KeyError:
@@ -251,36 +208,33 @@ def map_image_from_tar_oci_layout_v1(image_name: str, tar: TarFile, tar_location
251208
eprint(f"Image '{image_name}' is not found in '{tar_location}'")
252209

253210

254-
def map_image_from_tar(image_name: str, tar: TarFile, tar_location: str):
255-
tar_dir = os.path.dirname(tar_location)
211+
def map_image_from_tar(image_name: str, tar: TarFile, _tar_location: str):
212+
# Inspect the manifest file and see if any of the RepoTags match the
213+
# image_name we're searching for. For each manifest in the JSON, it should
214+
# also have a Config field for what json files they map to to get env vars,
215+
# startup cmd, etc.
216+
#
217+
# NOTE: read manifest.json directly (not via read_file_from_tar) so that a
218+
# missing manifest.json raises KeyError. The caller relies on that to fall
219+
# back to the OCI layout v1 reader.
220+
manifest_bytes = tar.extractfile("manifest.json").read()
221+
manifest = load_json_from_str(manifest_bytes)
222+
256223
info_file = None
257-
info_file_name = "manifest.json"
258-
259-
# extract just the manifest file and see if any of the RepoTags match the image_name we're searching for
260-
# the manifest.json should have a list of all the image tags
261-
# and what json files they map to to get env vars, startup cmd, etc.
262-
tar.extract(info_file_name, path=tar_dir)
263-
manifest_path = os.path.join(tar_dir, info_file_name)
264-
manifest = load_json_from_file(manifest_path)
265-
try:
266-
# if we match a RepoTag to the image, stop searching
267-
for image in manifest:
268-
if image_name in image.get("RepoTags"):
269-
info_file = image.get("Config")
270-
break
271-
finally:
272-
# remove the extracted manifest file to clean up
273-
force_delete_silently(manifest_path)
224+
# if we match a RepoTag to the image, stop searching
225+
for image in manifest:
226+
if image_name in image.get("RepoTags"):
227+
info_file = image.get("Config")
228+
break
274229

275230
if not info_file:
276231
return None
277-
tar.extract(info_file, path=tar_dir)
278232

279-
# get the path of the json file and read it in
280-
image_info_file_path = os.path.join(tar_dir, info_file)
281-
image_info_raw = load_json_from_file(image_info_file_path)
282-
# delete the extracted json file to clean up
283-
force_delete_silently(image_info_file_path)
233+
# Read config file directly from the tar stream (without extracting
234+
# anything) so that malicious paths in the manifest cannot cause any actual
235+
# writes.
236+
image_info_raw_bytes = read_file_from_tar(tar, info_file)
237+
image_info_raw = load_json_from_str(image_info_raw_bytes)
284238
image_info = image_info_raw.get("config")
285239
# importing the constant from config.py gives a circular dependency error
286240
image_info["Architecture"] = image_info_raw.get("architecture")

src/confcom/azext_confcom/rootfs_proxy.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,13 @@
2828
_dmverity_vhd_binaries = {
2929
"Linux": {
3030
"path": _binaries_dir / "dmverity-vhd",
31-
"url": "https://github.com/microsoft/integrity-vhd/releases/download/v2.0/dmverity-vhd",
32-
"sha256": "e7ad858fef018acd7d8a4ccb74f1b7a9cc1b3d6db5a7f8da5a259f71b26c12ea",
31+
"url": "https://github.com/microsoft/integrity-vhd/releases/download/v2.1/dmverity-vhd",
32+
"sha256": "a75eb11f3ad3058bfdef0b5cf0bf64c1bef714a2afa054f3e242932d25d9e57d",
3333
},
3434
"Windows": {
3535
"path": _binaries_dir / "dmverity-vhd.exe",
36-
"url": "https://github.com/microsoft/integrity-vhd/releases/download/v2.0/dmverity-vhd.exe",
37-
"sha256": "6ef425c4bd07739d9cc90e57488985c1fca41f8d106fc816123b95b6305ee0af",
36+
"url": "https://github.com/microsoft/integrity-vhd/releases/download/v2.1/dmverity-vhd.exe",
37+
"sha256": "5e371f86a2b552e5e69759421a81a26a07450dc404c1c88a08a4f983322598a2",
3838
},
3939
}
4040

src/confcom/azext_confcom/template_util.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -107,11 +107,6 @@ def get_image_info(progress, message_queue, tar_mapping, image):
107107
with tarfile.open(tar_location) as tar_file:
108108
# get all the info out of the tarfile
109109
try:
110-
logger.info("using backwards compatibility tar file")
111-
image_info = os_util.map_image_from_tar_backwards_compatibility(
112-
image_name, tar_file, tar_location
113-
)
114-
except IndexError:
115110
logger.info("using docker formatted tar file")
116111
image_info = os_util.map_image_from_tar(
117112
image_name, tar_file, tar_location

src/confcom/setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
logger.warn("Wheel is not available, disabling bdist_wheel hook")
2121

22-
VERSION = "2.0.0b2"
22+
VERSION = "2.0.0"
2323

2424
# The full list of classifiers is available at
2525
# https://pypi.python.org/pypi?%3Aaction=list_classifiers

0 commit comments

Comments
 (0)