win_unzip - normalize and compare paths to prevent path traversal (#67799)
* Actually inspect the paths and prevent escape * Add integration tests * Generate zip files for use in integration test * Adjust error message
This commit is contained in:
parent
8ab304af44
commit
d30c57ab22
4 changed files with 134 additions and 1 deletions
4
changelogs/fragments/win-unzip-check-extraction-path.yml
Normal file
4
changelogs/fragments/win-unzip-check-extraction-path.yml
Normal file
|
@ -0,0 +1,4 @@
|
|||
bugfixes:
|
||||
- >
|
||||
**security issue** win_unzip - normalize paths in archive to ensure extracted
|
||||
files do not escape from the target directory (CVE-2020-1737)
|
|
@ -40,6 +40,15 @@ Function Extract-Zip($src, $dest) {
|
|||
$entry_target_path = [System.IO.Path]::Combine($dest, $archive_name)
|
||||
$entry_dir = [System.IO.Path]::GetDirectoryName($entry_target_path)
|
||||
|
||||
# Normalize paths for further evaluation
|
||||
$full_target_path = [System.IO.Path]::GetFullPath($entry_target_path)
|
||||
$full_dest_path = [System.IO.Path]::GetFullPath($dest + [System.IO.Path]::DirectorySeparatorChar)
|
||||
|
||||
# Ensure file in the archive does not escape the extraction path
|
||||
if (-not $full_target_path.StartsWith($full_dest_path)) {
|
||||
Fail-Json -obj $result -message "Error unzipping '$src' to '$dest'! Filename contains relative paths which would extract outside the destination: $entry_target_path"
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $entry_dir)) {
|
||||
New-Item -Path $entry_dir -ItemType Directory -WhatIf:$check_mode | Out-Null
|
||||
$result.changed = $true
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2020 Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import zipfile
|
||||
|
||||
# Each key is a zip file and the vaule is the list of files that will be created
|
||||
# and placed in the archive
|
||||
zip_files = {
|
||||
'hat1': [r'hat/..\rabbit.txt'],
|
||||
'hat2': [r'hat/..\..\rabbit.txt'],
|
||||
'handcuffs': [r'..\..\houidini.txt'],
|
||||
'prison': [r'..\houidini.txt'],
|
||||
}
|
||||
|
||||
# Accept an argument of where to create the files, defaulting to
|
||||
# the current working directory.
|
||||
try:
|
||||
output_dir = sys.argv[1]
|
||||
except IndexError:
|
||||
output_dir = os.getcwd()
|
||||
|
||||
if not os.path.isdir(output_dir):
|
||||
os.mkdir(output_dir)
|
||||
|
||||
os.chdir(output_dir)
|
||||
|
||||
for name, files in zip_files.items():
|
||||
# Create the files to go in the zip archive
|
||||
for entry in files:
|
||||
dirname = os.path.dirname(entry)
|
||||
if dirname:
|
||||
if os.path.isdir(dirname):
|
||||
shutil.rmtree(dirname)
|
||||
os.mkdir(dirname)
|
||||
|
||||
with open(entry, 'w') as e:
|
||||
e.write('escape!\n')
|
||||
|
||||
# Create the zip archive with the files
|
||||
filename = '%s.zip' % name
|
||||
if os.path.isfile(filename):
|
||||
os.unlink(filename)
|
||||
|
||||
with zipfile.ZipFile(filename, 'w') as zf:
|
||||
for entry in files:
|
||||
zf.write(entry)
|
||||
|
||||
# Cleanup
|
||||
if dirname:
|
||||
shutil.rmtree(dirname)
|
||||
|
||||
for entry in files:
|
||||
try:
|
||||
os.unlink(entry)
|
||||
except OSError:
|
||||
pass
|
|
@ -1,4 +1,3 @@
|
|||
---
|
||||
- name: create test directory
|
||||
win_file:
|
||||
path: '{{ win_unzip_dir }}\output'
|
||||
|
@ -114,3 +113,59 @@
|
|||
- unzip_delete is changed
|
||||
- unzip_delete.removed
|
||||
- not unzip_delete_actual.stat.exists
|
||||
|
||||
# Path traversal tests (CVE-2020-1737)
|
||||
- name: Create zip files
|
||||
script: create_crafty_zip_files.py {{ output_dir }}
|
||||
delegate_to: localhost
|
||||
|
||||
- name: Copy zip files to Windows host
|
||||
win_copy:
|
||||
src: "{{ output_dir }}/{{ item }}.zip"
|
||||
dest: "{{ win_unzip_dir }}/"
|
||||
loop:
|
||||
- hat1
|
||||
- hat2
|
||||
- handcuffs
|
||||
- prison
|
||||
|
||||
- name: Perform first trick
|
||||
win_unzip:
|
||||
src: '{{ win_unzip_dir }}\hat1.zip'
|
||||
dest: '{{ win_unzip_dir }}\output'
|
||||
register: hat_trick1
|
||||
|
||||
- name: Check for file
|
||||
win_stat:
|
||||
path: '{{ win_unzip_dir }}\output\rabbit.txt'
|
||||
register: rabbit
|
||||
|
||||
- name: Perform next tricks (which should all fail)
|
||||
win_unzip:
|
||||
src: '{{ win_unzip_dir }}\{{ item }}.zip'
|
||||
dest: '{{ win_unzip_dir }}\output'
|
||||
ignore_errors: yes
|
||||
register: escape
|
||||
loop:
|
||||
- hat2
|
||||
- handcuffs
|
||||
- prison
|
||||
|
||||
- name: Search for files
|
||||
win_find:
|
||||
recurse: yes
|
||||
paths:
|
||||
- '{{ win_unzip_dir }}'
|
||||
patterns:
|
||||
- '*houdini.txt'
|
||||
- '*rabbit.txt'
|
||||
register: files
|
||||
|
||||
- name: Check results
|
||||
assert:
|
||||
that:
|
||||
- rabbit.stat.exists
|
||||
- hat_trick1 is success
|
||||
- escape.results | map(attribute='failed') | unique | list == [True]
|
||||
- files.matched == 1
|
||||
- files.files[0]['filename'] == 'rabbit.txt'
|
||||
|
|
Loading…
Reference in a new issue