##// END OF EJS Templates
fix(git-lfs): fixed security problem with allowing off-chain attacks to replace OID data without validating hash for already present oids....
super-admin -
r1300:a680a605 default
parent child Browse files
Show More
@@ -14,7 +14,7 b''
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software Foundation,
16 16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17 import hashlib
18 18 import re
19 19 import logging
20 20
@@ -160,8 +160,14 b' def lfs_objects_oid_upload(request):'
160 160 engine = store.get_engine(mode='wb')
161 161 log.debug('LFS: starting chunked write of LFS oid: %s to storage', oid)
162 162
163 # validate if OID is not by any chance already in the store
164 if store.has_oid():
165 log.debug('LFS: oid %s exists in store', oid)
166 return {'upload': 'ok', 'state': 'in-store'}
167
163 168 body = request.environ['wsgi.input']
164 169
170 digest = hashlib.sha256()
165 171 with engine as f:
166 172 blksize = 64 * 1024 # 64kb
167 173 while True:
@@ -175,10 +181,17 b' def lfs_objects_oid_upload(request):'
175 181
176 182 if not chunk:
177 183 break
184 f.write(chunk)
185 digest.update(chunk)
178 186
179 f.write(chunk)
187 hex_digest = digest.hexdigest()
188 digest_check = hex_digest == oid
189 if not digest_check:
190 engine.cleanup() # trigger cleanup so we don't save mismatch OID into the store
191 return write_response_error(
192 HTTPBadRequest, f'oid {oid} does not match expected sha {hex_digest}')
180 193
181 return {'upload': 'ok'}
194 return {'upload': 'ok', 'state': 'written'}
182 195
183 196
184 197 def lfs_objects_oid_download(request):
@@ -223,12 +236,10 b' def lfs_objects_verify(request):'
223 236
224 237 store_size = store.size_oid()
225 238 if store_size != size:
226 msg = 'requested file size mismatch store size:{} requested:{}'.format(
227 store_size, size)
228 return write_response_error(
229 HTTPUnprocessableEntity, msg)
239 msg = f'requested file size mismatch store size:{store_size} requested:{size}'
240 return write_response_error(HTTPUnprocessableEntity, msg)
230 241
231 return {'message': {'size': 'ok', 'in_store': 'ok'}}
242 return {'message': {'size': store_size, 'oid': oid}}
232 243
233 244
234 245 def lfs_objects_lock(request):
@@ -133,12 +133,16 b' class LFSOidStore:'
133 133 """
134 134
135 135 class StoreEngine:
136 _cleanup = None
136 137 def __init__(self, mode, store_path, oid_path, tmp_oid_path):
137 138 self.mode = mode
138 139 self.store_path = store_path
139 140 self.oid_path = oid_path
140 141 self.tmp_oid_path = tmp_oid_path
141 142
143 def cleanup(self):
144 self._cleanup = True
145
142 146 def __enter__(self):
143 147 if not os.path.isdir(self.store_path):
144 148 os.makedirs(self.store_path)
@@ -149,9 +153,13 b' class LFSOidStore:'
149 153 return fd
150 154
151 155 def __exit__(self, exc_type, exc_value, traceback):
156 self.fd.close()
157
158 if self._cleanup is None:
152 159 # close tmp file, and rename to final destination
153 self.fd.close()
154 160 shutil.move(self.tmp_oid_path, self.oid_path)
161 else:
162 os.remove(self.tmp_oid_path)
155 163
156 164 return StoreEngine(
157 165 mode, self.store_path, self.oid_path, self.tmp_oid_path)
@@ -100,22 +100,27 b' class TestLFSApplication:'
100 100
101 101 def test_app_batch_api_download_missing_object(
102 102 self, git_lfs_app, http_auth):
103 params = {'operation': 'download',
104 'objects': [{'oid': '123', 'size': '1024'}]}
103 params = {
104 'operation': 'download',
105 'objects': [{'oid': '123', 'size': '1024'}]
106 }
105 107 response = git_lfs_app.post_json(
106 108 '/repo/info/lfs/objects/batch', params=params,
107 109 extra_environ=http_auth)
108 110
109 111 expected_objects = [
110 {'authenticated': True,
111 'errors': {'error': {
112 'code': 404,
113 'message': 'object: 123 does not exist in store'}},
112 {
114 113 'oid': '123',
115 'size': '1024'}
114 'size': '1024',
115 'authenticated': True,
116 'errors': {'error': {'code': 404, 'message': 'object: 123 does not exist in store'}},
117 }
116 118 ]
119
117 120 assert json.loads(response.text) == {
118 'objects': expected_objects, 'transfer': 'basic'}
121 'objects': expected_objects,
122 'transfer': 'basic'
123 }
119 124
120 125 def test_app_batch_api_download(self, git_lfs_app, http_auth):
121 126 oid = '456'
@@ -142,7 +147,9 b' class TestLFSApplication:'
142 147 'size': '1024'}
143 148 ]
144 149 assert json.loads(response.text) == {
145 'objects': expected_objects, 'transfer': 'basic'}
150 'objects': expected_objects,
151 'transfer': 'basic'
152 }
146 153
147 154 def test_app_batch_api_upload(self, git_lfs_app, http_auth):
148 155 params = {'operation': 'upload',
@@ -151,21 +158,31 b' class TestLFSApplication:'
151 158 '/repo/info/lfs/objects/batch', params=params,
152 159 extra_environ=http_auth)
153 160 expected_objects = [
154 {'authenticated': True,
161 {
162 'authenticated': True,
155 163 'actions': {
156 164 'upload': {
157 'header': {'Authorization': 'Basic XXXXX',
158 'Transfer-Encoding': 'chunked'},
159 'href': 'http://localhost/repo/info/lfs/objects/123'},
165 'header': {
166 'Authorization': 'Basic XXXXX',
167 'Transfer-Encoding': 'chunked'
168 },
169 'href': 'http://localhost/repo/info/lfs/objects/123'
170 },
160 171 'verify': {
161 'header': {'Authorization': 'Basic XXXXX'},
162 'href': 'http://localhost/repo/info/lfs/verify'}
172 'header': {
173 'Authorization': 'Basic XXXXX'
174 },
175 'href': 'http://localhost/repo/info/lfs/verify'
176 }
163 177 },
164 178 'oid': '123',
165 'size': '1024'}
179 'size': '1024'
180 }
166 181 ]
167 182 assert json.loads(response.text) == {
168 'objects': expected_objects, 'transfer': 'basic'}
183 'objects': expected_objects,
184 'transfer': 'basic'
185 }
169 186
170 187 def test_app_batch_api_upload_for_https(self, git_lfs_https_app, http_auth):
171 188 params = {'operation': 'upload',
@@ -206,7 +223,8 b' class TestLFSApplication:'
206 223 status=404)
207 224
208 225 assert json.loads(response.text) == {
209 'message': 'oid `missing` does not exists in store'}
226 'message': 'oid `missing` does not exists in store'
227 }
210 228
211 229 def test_app_verify_api_size_mismatch(self, git_lfs_app):
212 230 oid = 'existing'
@@ -221,8 +239,8 b' class TestLFSApplication:'
221 239 '/repo/info/lfs/verify', params=params, status=422)
222 240
223 241 assert json.loads(response.text) == {
224 'message': 'requested file size mismatch '
225 'store size:11 requested:1024'}
242 'message': 'requested file size mismatch store size:11 requested:1024'
243 }
226 244
227 245 def test_app_verify_api(self, git_lfs_app):
228 246 oid = 'existing'
@@ -237,13 +255,13 b' class TestLFSApplication:'
237 255 '/repo/info/lfs/verify', params=params)
238 256
239 257 assert json.loads(response.text) == {
240 'message': {'size': 'ok', 'in_store': 'ok'}}
258 'message': {'size': 11, 'oid': oid}
259 }
241 260
242 261 def test_app_download_api_oid_not_existing(self, git_lfs_app):
243 262 oid = 'missing'
244 263
245 response = git_lfs_app.get(
246 '/repo/info/lfs/objects/{oid}'.format(oid=oid), status=404)
264 response = git_lfs_app.get(f'/repo/info/lfs/objects/{oid}', status=404)
247 265
248 266 assert json.loads(response.text) == {
249 267 'message': 'requested file with oid `missing` not found in store'}
@@ -256,19 +274,37 b' class TestLFSApplication:'
256 274 with open(oid_path, 'wb') as f:
257 275 f.write(safe_bytes('OID_CONTENT'))
258 276
259 response = git_lfs_app.get(
260 '/repo/info/lfs/objects/{oid}'.format(oid=oid))
277 response = git_lfs_app.get(f'/repo/info/lfs/objects/{oid}')
261 278 assert response
262 279
263 280 def test_app_upload(self, git_lfs_app):
264 oid = 'uploaded'
281 oid = '65f23e22a9bfedda96929b3cfcb8b6d2fdd34a2e877ddb81f45d79ab05710e12'
265 282
266 283 response = git_lfs_app.put(
267 '/repo/info/lfs/objects/{oid}'.format(oid=oid), params='CONTENT')
284 f'/repo/info/lfs/objects/{oid}', params='CONTENT')
268 285
269 assert json.loads(response.text) == {'upload': 'ok'}
286 assert json.loads(response.text) == {'upload': 'ok', 'state': 'written'}
270 287
271 288 # verify that we actually wrote that OID
272 289 oid_path = LFSOidStore(oid=oid, repo=None, store_location=git_lfs_app._store).oid_path
273 290 assert os.path.isfile(oid_path)
274 291 assert 'CONTENT' == open(oid_path).read()
292
293 response = git_lfs_app.put(
294 f'/repo/info/lfs/objects/{oid}', params='CONTENT')
295
296 assert json.loads(response.text) == {'upload': 'ok', 'state': 'in-store'}
297
298
299 def test_app_upload_wrong_sha(self, git_lfs_app):
300 oid = 'i-am-a-wrong-sha'
301
302 response = git_lfs_app.put(f'/repo/info/lfs/objects/{oid}', params='CONTENT', status=400)
303
304 assert json.loads(response.text) == {
305 'message': 'oid i-am-a-wrong-sha does not match expected sha '
306 '65f23e22a9bfedda96929b3cfcb8b6d2fdd34a2e877ddb81f45d79ab05710e12'}
307
308 # check this OID wasn't written to store
309 response = git_lfs_app.get(f'/repo/info/lfs/objects/{oid}', status=404)
310 assert json.loads(response.text) == {'message': 'requested file with oid `i-am-a-wrong-sha` not found in store'}
@@ -24,7 +24,7 b' from vcsserver.git_lfs.lib import OidHan'
24 24 @pytest.fixture()
25 25 def lfs_store(tmpdir):
26 26 repo = 'test'
27 oid = '123456789'
27 oid = '65f23e22a9bfedda96929b3cfcb8b6d2fdd34a2e877ddb81f45d79ab05710e12'
28 28 store = LFSOidStore(oid=oid, repo=repo, store_location=str(tmpdir))
29 29 return store
30 30
@@ -63,7 +63,8 b' class TestOidHandler:'
63 63 assert response is None
64 64 assert has_errors['error'] == {
65 65 'code': 404,
66 'message': 'object: 123456789 does not exist in store'}
66 'message': 'object: 65f23e22a9bfedda96929b3cfcb8b6d2fdd34a2e877ddb81f45d79ab05710e12 does not exist in store'
67 }
67 68
68 69 def test_download_oid(self, oid_handler):
69 70 store = oid_handler.get_store()
General Comments 0
You need to be logged in to leave comments. Login now