Merge branch 'pr40': preserve file perms in directories
This commit is contained in:
commit
54b85035a2
|
@ -223,7 +223,7 @@ class TwistedReceiver:
|
||||||
destname = os.path.basename(destname)
|
destname = os.path.basename(destname)
|
||||||
if self.args.output_file:
|
if self.args.output_file:
|
||||||
destname = self.args.output_file # override
|
destname = self.args.output_file # override
|
||||||
abs_destname = os.path.join(self.args.cwd, destname)
|
abs_destname = os.path.abspath( os.path.join(self.args.cwd, destname) )
|
||||||
|
|
||||||
# get confirmation from the user before writing to the local directory
|
# get confirmation from the user before writing to the local directory
|
||||||
if os.path.exists(abs_destname):
|
if os.path.exists(abs_destname):
|
||||||
|
@ -284,15 +284,31 @@ class TwistedReceiver:
|
||||||
self._msg(u"Received file written to %s" %
|
self._msg(u"Received file written to %s" %
|
||||||
os.path.basename(self.abs_destname))
|
os.path.basename(self.abs_destname))
|
||||||
|
|
||||||
|
def _extract_file(self, zf, info, extract_dir):
|
||||||
|
"""
|
||||||
|
the zipfile module does not restore file permissions
|
||||||
|
so we'll do it manually
|
||||||
|
"""
|
||||||
|
out_path = os.path.join( extract_dir, info.filename )
|
||||||
|
out_path = os.path.abspath( out_path )
|
||||||
|
if not out_path.startswith( extract_dir ):
|
||||||
|
raise ValueError( "malicious zipfile, %s outside of extract_dir %s"
|
||||||
|
% (info.filename, extract_dir) )
|
||||||
|
|
||||||
|
zf.extract( info.filename, path=extract_dir )
|
||||||
|
|
||||||
|
# not sure why zipfiles store the perms 16 bits away but they do
|
||||||
|
perm = info.external_attr >> 16
|
||||||
|
os.chmod( out_path, perm )
|
||||||
|
|
||||||
def _write_directory(self, f):
|
def _write_directory(self, f):
|
||||||
|
|
||||||
self._msg(u"Unpacking zipfile..")
|
self._msg(u"Unpacking zipfile..")
|
||||||
with self.args.timing.add("unpack zip"):
|
with self.args.timing.add("unpack zip"):
|
||||||
with zipfile.ZipFile(f, "r", zipfile.ZIP_DEFLATED) as zf:
|
with zipfile.ZipFile(f, "r", zipfile.ZIP_DEFLATED) as zf:
|
||||||
zf.extractall(path=self.abs_destname)
|
for info in zf.infolist():
|
||||||
# extractall() appears to offer some protection against
|
self._extract_file( zf, info, self.abs_destname )
|
||||||
# malicious pathnames. For example, "/tmp/oops" and
|
|
||||||
# "../tmp/oops" both do the same thing as the (safe)
|
|
||||||
# "tmp/oops".
|
|
||||||
self._msg(u"Received files written to %s/" %
|
self._msg(u"Received files written to %s/" %
|
||||||
os.path.basename(self.abs_destname))
|
os.path.basename(self.abs_destname))
|
||||||
f.close()
|
f.close()
|
||||||
|
|
|
@ -197,7 +197,6 @@ class Sender:
|
||||||
# We're sending a directory. Create a zipfile in a tempdir and
|
# We're sending a directory. Create a zipfile in a tempdir and
|
||||||
# send that.
|
# send that.
|
||||||
fd_to_send = tempfile.SpooledTemporaryFile()
|
fd_to_send = tempfile.SpooledTemporaryFile()
|
||||||
# TODO: I think ZIP_DEFLATED means compressed.. check it
|
|
||||||
num_files = 0
|
num_files = 0
|
||||||
num_bytes = 0
|
num_bytes = 0
|
||||||
tostrip = len(what.split(os.sep))
|
tostrip = len(what.split(os.sep))
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
import os, sys, re, io, zipfile, six
|
import os, sys, re, io, zipfile, six, stat
|
||||||
|
import mock
|
||||||
from twisted.trial import unittest
|
from twisted.trial import unittest
|
||||||
from twisted.python import procutils, log
|
from twisted.python import procutils, log
|
||||||
from twisted.internet.utils import getProcessOutputAndValue
|
from twisted.internet.utils import getProcessOutputAndValue
|
||||||
|
@ -287,9 +288,14 @@ class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase):
|
||||||
os.mkdir(os.path.join(send_dir, "middle"))
|
os.mkdir(os.path.join(send_dir, "middle"))
|
||||||
source_dir = os.path.join(send_dir, "middle", send_dirname)
|
source_dir = os.path.join(send_dir, "middle", send_dirname)
|
||||||
os.mkdir(source_dir)
|
os.mkdir(source_dir)
|
||||||
|
modes = {}
|
||||||
for i in range(5):
|
for i in range(5):
|
||||||
with open(os.path.join(source_dir, str(i)), "w") as f:
|
path = os.path.join(source_dir, str(i))
|
||||||
|
with open(path, "w") as f:
|
||||||
f.write(message(i))
|
f.write(message(i))
|
||||||
|
if i == 3:
|
||||||
|
os.chmod(path, 0o755)
|
||||||
|
modes[i] = stat.S_IMODE(os.stat(path).st_mode)
|
||||||
send_dirname_arg = os.path.join("middle", send_dirname)
|
send_dirname_arg = os.path.join("middle", send_dirname)
|
||||||
if addslash:
|
if addslash:
|
||||||
send_dirname_arg += os.sep
|
send_dirname_arg += os.sep
|
||||||
|
@ -412,6 +418,8 @@ class PregeneratedCode(ServerBase, ScriptsBase, unittest.TestCase):
|
||||||
fn = os.path.join(receive_dir, receive_dirname, str(i))
|
fn = os.path.join(receive_dir, receive_dirname, str(i))
|
||||||
with open(fn, "r") as f:
|
with open(fn, "r") as f:
|
||||||
self.failUnlessEqual(f.read(), message(i))
|
self.failUnlessEqual(f.read(), message(i))
|
||||||
|
self.failUnlessEqual(modes[i],
|
||||||
|
stat.S_IMODE(os.stat(fn).st_mode))
|
||||||
|
|
||||||
def test_text(self):
|
def test_text(self):
|
||||||
return self._do_test()
|
return self._do_test()
|
||||||
|
@ -613,3 +621,44 @@ class Cleanup(ServerBase, unittest.TestCase):
|
||||||
self.assertEqual(len(cids), 0)
|
self.assertEqual(len(cids), 0)
|
||||||
self.flushLoggedErrors(WrongPasswordError)
|
self.flushLoggedErrors(WrongPasswordError)
|
||||||
|
|
||||||
|
class ExtractFile(unittest.TestCase):
|
||||||
|
def test_filenames(self):
|
||||||
|
args = mock.Mock()
|
||||||
|
args.relay_url = u""
|
||||||
|
ef = cmd_receive.TwistedReceiver(args)._extract_file
|
||||||
|
extract_dir = os.path.abspath(self.mktemp())
|
||||||
|
|
||||||
|
zf = mock.Mock()
|
||||||
|
zi = mock.Mock()
|
||||||
|
zi.filename = "ok"
|
||||||
|
zi.external_attr = 5 << 16
|
||||||
|
expected = os.path.join(extract_dir, "ok")
|
||||||
|
with mock.patch.object(cmd_receive.os, "chmod") as chmod:
|
||||||
|
ef(zf, zi, extract_dir)
|
||||||
|
self.assertEqual(zf.extract.mock_calls,
|
||||||
|
[mock.call(zi.filename, path=extract_dir)])
|
||||||
|
self.assertEqual(chmod.mock_calls, [mock.call(expected, 5)])
|
||||||
|
|
||||||
|
zf = mock.Mock()
|
||||||
|
zi = mock.Mock()
|
||||||
|
zi.filename = "../haha"
|
||||||
|
e = self.assertRaises(ValueError, ef, zf, zi, extract_dir)
|
||||||
|
self.assertIn("malicious zipfile", str(e))
|
||||||
|
|
||||||
|
zf = mock.Mock()
|
||||||
|
zi = mock.Mock()
|
||||||
|
zi.filename = "haha//root" # abspath squashes this, hopefully zipfile
|
||||||
|
# does too
|
||||||
|
zi.external_attr = 5 << 16
|
||||||
|
expected = os.path.join(extract_dir, "haha/root")
|
||||||
|
with mock.patch.object(cmd_receive.os, "chmod") as chmod:
|
||||||
|
ef(zf, zi, extract_dir)
|
||||||
|
self.assertEqual(zf.extract.mock_calls,
|
||||||
|
[mock.call(zi.filename, path=extract_dir)])
|
||||||
|
self.assertEqual(chmod.mock_calls, [mock.call(expected, 5)])
|
||||||
|
|
||||||
|
zf = mock.Mock()
|
||||||
|
zi = mock.Mock()
|
||||||
|
zi.filename = "/etc/passwd"
|
||||||
|
e = self.assertRaises(ValueError, ef, zf, zi, extract_dir)
|
||||||
|
self.assertIn("malicious zipfile", str(e))
|
||||||
|
|
Loading…
Reference in New Issue
Block a user