Merge lp:~adeuring/charmworld/check-symlinks into lp:~juju-jitsu/charmworld/trunk

Proposed by Abel Deuring
Status: Merged
Approved by: Curtis Hovey
Approved revision: 222
Merged at revision: 222
Proposed branch: lp:~adeuring/charmworld/check-symlinks
Merge into: lp:~juju-jitsu/charmworld/trunk
Diff against target: 223 lines (+95/-19)
3 files modified
charmworld/jobs/ingest.py (+1/-1)
charmworld/models.py (+35/-13)
charmworld/test_models.py (+59/-5)
To merge this branch: bzr merge lp:~adeuring/charmworld/check-symlinks
Reviewer Review Type Date Requested Status
Curtis Hovey (community) code Approve
Review via email: mp+162381@code.launchpad.net

Commit message

CharmFileSet.save_file(): Check if the target of a symlink exists and if the target is within the charm.

Description of the change

This branch fixes bug 1172747: File ingestion assumes symlinks are not broken.

I added two checks to CharmFileSet.save_file(): That the target of a symlink exists and that it is not outside the charm's root directory.

If such a bad symlink is found, a warning is logged, and no data is stored for this file.

To post a comment you must log in.
Revision history for this message
Curtis Hovey (sinzui) wrote :

Thank you.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'charmworld/jobs/ingest.py'
2--- charmworld/jobs/ingest.py 2013-04-30 20:11:59 +0000
3+++ charmworld/jobs/ingest.py 2013-05-03 15:54:12 +0000
4@@ -112,7 +112,7 @@
5 """
6 self.log.info('Storing files of branch into gridfs')
7 filestore = CharmFileSet.save_files(
8- self.fs, charm_data, charm_data['branch_dir'])
9+ self.fs, charm_data, charm_data['branch_dir'], self.log)
10 self.log.info('Completed gridfs storage.')
11 return filestore
12
13
14=== modified file 'charmworld/models.py'
15--- charmworld/models.py 2013-04-26 20:33:30 +0000
16+++ charmworld/models.py 2013-05-03 15:54:12 +0000
17@@ -6,8 +6,10 @@
18 import logging
19 from os import listdir
20 from os.path import dirname
21+from os.path import exists
22 from os.path import isdir
23 from os.path import join
24+from os.path import realpath
25 import random
26 import re
27
28@@ -276,7 +278,8 @@
29 )
30
31 @classmethod
32- def save_file(cls, fs, charm_data, filename, local_directory, subdir=None):
33+ def save_file(cls, fs, logger, charm_data, filename, local_directory,
34+ subdir=None):
35 """Do the actual storage of the file contents.
36
37 :param fs: The mongo fs connection to use.
38@@ -285,7 +288,23 @@
39 :param directory: The directory is this file located in on disk.
40
41 """
42-
43+ charm_root = realpath(local_directory)
44+ if subdir is not None:
45+ file_path = join(local_directory, subdir, filename)
46+ else:
47+ file_path = join(local_directory, filename)
48+ real_file_path = realpath(file_path)
49+ if not real_file_path.startswith(charm_root):
50+ logger.warn(
51+ "Invalid file path: Path %s resolves to %s, which is not "
52+ "inside the charm's root directory %s" % (
53+ file_path, real_file_path, charm_root))
54+ return
55+ if not exists(real_file_path):
56+ logger.warn(
57+ "File does not exist: %s (resolving to %s)." % (
58+ file_path, real_file_path))
59+ return
60 if subdir:
61 complete_filename = join(subdir, filename)
62 else:
63@@ -334,7 +353,7 @@
64 return cf
65
66 @classmethod
67- def save_files(cls, fs, charm_data, charm_bzr_path):
68+ def save_files(cls, fs, charm_data, charm_bzr_path, logger):
69 """Given a bzr branch, load the files we care about."""
70 root_files = listdir(charm_bzr_path)
71 stored = []
72@@ -348,12 +367,14 @@
73 found_filename = filename
74
75 if found_filename:
76- stored.append(
77- cls.save_file(
78- fs,
79- charm_data,
80- found_filename,
81- charm_bzr_path))
82+ charm_file = cls.save_file(
83+ fs,
84+ logger,
85+ charm_data,
86+ found_filename,
87+ charm_bzr_path)
88+ if charm_file is not None:
89+ stored.append(charm_file)
90
91 for directory in cls.store_directories:
92 dir_path = join(charm_bzr_path, directory)
93@@ -366,10 +387,11 @@
94
95 for filename in dir_files:
96 if not isdir(join(dir_path, filename)):
97- stored.append(
98- cls.save_file(
99- fs, charm_data, filename, charm_bzr_path,
100- subdir=directory))
101+ charm_file = cls.save_file(
102+ fs, logger, charm_data, filename, charm_bzr_path,
103+ subdir=directory)
104+ if charm_file is not None:
105+ stored.append(charm_file)
106 return stored
107
108
109
110=== modified file 'charmworld/test_models.py'
111--- charmworld/test_models.py 2013-04-29 20:13:31 +0000
112+++ charmworld/test_models.py 2013-05-03 15:54:12 +0000
113@@ -3,6 +3,8 @@
114 # the file LICENSE).
115
116 """Unit tests for our application models."""
117+import logging
118+import os
119 from os.path import dirname
120 from os.path import join
121
122@@ -195,6 +197,11 @@
123 'testing/data/sample_charm'
124 )
125
126+ def setUp(self):
127+ super(TestCharmFileSet, self).setUp()
128+ self.logger = logging.getLogger(self.__class__.__name__)
129+ self.log_handler = self.get_handler(self.logger.name)
130+
131 def test_fileid_generation(self):
132 """A charm must generate an unique fileid."""
133 self.assertEqual(
134@@ -207,7 +214,8 @@
135 CharmFileSet.save_files(
136 self.fs,
137 self.charm_data,
138- self.charm_path)
139+ self.charm_path,
140+ self.logger)
141
142 # There should be the following files stored into the gridfs
143 # filestore.
144@@ -238,7 +246,8 @@
145 CharmFileSet.save_files(
146 self.fs,
147 self.charm_data,
148- self.charm_path)
149+ self.charm_path,
150+ self.logger)
151
152 # Verify that the filename of the README is kept.
153 found = self.fs.get_last_version('README.md')
154@@ -251,7 +260,8 @@
155 CharmFileSet.save_files(
156 self.fs,
157 self.charm_data,
158- self.charm_path)
159+ self.charm_path,
160+ self.logger)
161
162 readme_id = self.base_fileid + quote_key('README.md')
163
164@@ -276,7 +286,8 @@
165 CharmFileSet.save_files(
166 self.fs,
167 self.charm_data,
168- self.charm_path)
169+ self.charm_path,
170+ self.logger)
171
172 install_id = self.base_fileid + 'hooks/install'
173
174@@ -295,7 +306,50 @@
175 files = CharmFileSet.save_files(
176 self.fs,
177 self.charm_data,
178- path)
179+ path,
180+ self.logger)
181+ self.assertEqual([], files)
182+
183+ def test_save_file_ignores_symlink_without_target(self):
184+ # Symlinks that point to nowhere are not saved.
185+ path = self.use_context(temp_dir())
186+ link_path = join(path, 'readme')
187+ target_name = 'does-not-exist'
188+ target_path = join(path, target_name)
189+ os.symlink(target_name, link_path)
190+ files = CharmFileSet.save_files(
191+ self.fs,
192+ self.charm_data,
193+ path,
194+ self.logger)
195+ expected = [
196+ 'File does not exist: %s (resolving to %s).' % (
197+ link_path, target_path)
198+ ]
199+ messages = [rec.msg for rec in self.log_handler.buffer]
200+ self.assertEqual(expected, messages)
201+ self.assertEqual([], files)
202+
203+ def test_save_file_ignores_symlink_with_outside_target(self):
204+ # Symlinks that point to a location outside the charm's root
205+ # directory are also ignored.
206+ path = self.use_context(temp_dir())
207+ link_path = join(path, 'readme')
208+ target_name = '../no-trespassing'
209+ target_path = join(dirname(path), 'no-trespassing')
210+ os.symlink(target_name, link_path)
211+ files = CharmFileSet.save_files(
212+ self.fs,
213+ self.charm_data,
214+ path,
215+ self.logger)
216+ expected = [
217+ "Invalid file path: Path %s resolves to %s, which is not inside "
218+ "the charm's root directory %s" % (
219+ link_path, target_path, path)
220+ ]
221+ messages = [rec.msg for rec in self.log_handler.buffer]
222+ self.assertEqual(expected, messages)
223 self.assertEqual([], files)
224
225

Subscribers

People subscribed via source and target branches