@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Store and verify content integrity checksums for files

Summary:
Ref T12470. This helps defuse attacks where an adversary can directly take control of whatever storage engine files are being stored in and change data there. These attacks would require a significant level of access.

Such attackers could potentially attack ranges of AES-256-CBC encrypted files by using Phabricator as a decryption oracle if they were also able to compromise a Phabricator account with read access to the files.

By storing a hash of the data (and, in the case of AES-256-CBC files, the IV) when we write files, and verifying it before we decrypt or read them, we can detect and prevent this kind of tampering.

This also helps detect mundane corruption and integrity issues.

Test Plan:
- Added unit tests.
- Uploaded new files, saw them get integrity hashes.
- Manually corrupted file data, saw it fail. Used `bin/files cat --salvage` to read it anyway.
- Tampered with IVs, saw integrity failures.

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T12470

Differential Revision: https://secure.phabricator.com/D17625

+135 -6
+2
src/__phutil_library_map__.php
··· 2762 2762 'PhabricatorFileImageProxyController' => 'applications/files/controller/PhabricatorFileImageProxyController.php', 2763 2763 'PhabricatorFileImageTransform' => 'applications/files/transform/PhabricatorFileImageTransform.php', 2764 2764 'PhabricatorFileInfoController' => 'applications/files/controller/PhabricatorFileInfoController.php', 2765 + 'PhabricatorFileIntegrityException' => 'applications/files/exception/PhabricatorFileIntegrityException.php', 2765 2766 'PhabricatorFileLightboxController' => 'applications/files/controller/PhabricatorFileLightboxController.php', 2766 2767 'PhabricatorFileLinkView' => 'view/layout/PhabricatorFileLinkView.php', 2767 2768 'PhabricatorFileListController' => 'applications/files/controller/PhabricatorFileListController.php', ··· 7891 7892 'PhabricatorFileImageProxyController' => 'PhabricatorFileController', 7892 7893 'PhabricatorFileImageTransform' => 'PhabricatorFileTransform', 7893 7894 'PhabricatorFileInfoController' => 'PhabricatorFileController', 7895 + 'PhabricatorFileIntegrityException' => 'Exception', 7894 7896 'PhabricatorFileLightboxController' => 'PhabricatorFileController', 7895 7897 'PhabricatorFileLinkView' => 'AphrontTagView', 7896 7898 'PhabricatorFileListController' => 'PhabricatorFileController',
+24
src/applications/files/engine/PhabricatorFileStorageEngine.php
··· 332 332 PhabricatorFileStorageFormat $format) { 333 333 334 334 $formatted_data = $this->readFile($file->getStorageHandle()); 335 + 336 + $known_integrity = $file->getIntegrityHash(); 337 + if ($known_integrity !== null) { 338 + $new_integrity = $this->newIntegrityHash($formatted_data, $format); 339 + if ($known_integrity !== $new_integrity) { 340 + throw new PhabricatorFileIntegrityException( 341 + pht( 342 + 'File data integrity check failed. Dark forces have corrupted '. 343 + 'or tampered with this file. The file data can not be read.')); 344 + } 345 + } 346 + 335 347 $formatted_data = array($formatted_data); 336 348 337 349 $data = ''; ··· 349 361 } 350 362 351 363 return array($data); 364 + } 365 + 366 + public function newIntegrityHash( 367 + $data, 368 + PhabricatorFileStorageFormat $format) { 369 + 370 + $data_hash = PhabricatorHash::digest($data); 371 + $format_hash = $format->newFormatIntegrityHash(); 372 + 373 + $full_hash = "{$data_hash}/{$format_hash}"; 374 + 375 + return PhabricatorHash::digest($full_hash); 352 376 } 353 377 354 378 }
+4
src/applications/files/engine/PhabricatorTestStorageEngine.php
··· 47 47 unset(self::$storage[$handle]); 48 48 } 49 49 50 + public function tamperWithFile($handle, $data) { 51 + self::$storage[$handle] = $data; 52 + } 53 + 50 54 }
+4
src/applications/files/exception/PhabricatorFileIntegrityException.php
··· 1 + <?php 2 + 3 + final class PhabricatorFileIntegrityException 4 + extends Exception {}
+14
src/applications/files/format/PhabricatorFileAES256StorageFormat.php
··· 56 56 return array($data); 57 57 } 58 58 59 + public function newFormatIntegrityHash() { 60 + $file = $this->getFile(); 61 + list($key_envelope, $iv_envelope) = $this->extractKeyAndIV($file); 62 + 63 + // NOTE: We include the IV in the format integrity hash. If we do not, 64 + // attackers can potentially forge the first block of decrypted data 65 + // in CBC mode if they are able to substitute a chosen IV and predict 66 + // the plaintext. (Normally, they can not tamper with the IV.) 67 + 68 + $input = self::FORMATKEY.'/iv:'.$iv_envelope->openEnvelope(); 69 + 70 + return PhabricatorHash::digest($input); 71 + } 72 + 59 73 public function newStorageProperties() { 60 74 // Generate a unique key and IV for this block of data. 61 75 $key_envelope = self::newAES256Key();
+4
src/applications/files/format/PhabricatorFileStorageFormat.php
··· 22 22 abstract public function newReadIterator($raw_iterator); 23 23 abstract public function newWriteIterator($raw_iterator); 24 24 25 + public function newFormatIntegrityHash() { 26 + return null; 27 + } 28 + 25 29 public function newStorageProperties() { 26 30 return array(); 27 31 }
+33
src/applications/files/format/__tests__/PhabricatorFileStorageFormatTestCase.php
··· 92 92 } 93 93 $this->assertEqual('cow jumped', $raw_data); 94 94 } 95 + 96 + public function testStorageTampering() { 97 + $engine = new PhabricatorTestStorageEngine(); 98 + 99 + $good = 'The cow jumped over the full moon.'; 100 + $evil = 'The cow slept quietly, honoring the glorious dictator.'; 101 + 102 + $params = array( 103 + 'name' => 'message.txt', 104 + 'storageEngines' => array( 105 + $engine, 106 + ), 107 + ); 108 + 109 + // First, write the file normally. 110 + $file = PhabricatorFile::newFromFileData($good, $params); 111 + $this->assertEqual($good, $file->loadFileData()); 112 + 113 + // As an adversary, tamper with the file. 114 + $engine->tamperWithFile($file->getStorageHandle(), $evil); 115 + 116 + // Attempts to read the file data should now fail the integrity check. 117 + $caught = null; 118 + try { 119 + $file->loadFileData(); 120 + } catch (PhabricatorFileIntegrityException $ex) { 121 + $caught = $ex; 122 + } 123 + 124 + $this->assertTrue($caught instanceof PhabricatorFileIntegrityException); 125 + } 126 + 127 + 95 128 }
+29 -3
src/applications/files/management/PhabricatorFilesManagementCatWorkflow.php
··· 20 20 'help' => pht('End printing at a specific offset.'), 21 21 ), 22 22 array( 23 + 'name' => 'salvage', 24 + 'help' => pht( 25 + 'DANGEROUS. Attempt to salvage file content even if the '. 26 + 'integrity check fails. If an adversary has tampered with '. 27 + 'the file, the conent may be unsafe.'), 28 + ), 29 + array( 23 30 'name' => 'names', 24 31 'wildcard' => true, 25 32 ), ··· 43 50 $begin = $args->getArg('begin'); 44 51 $end = $args->getArg('end'); 45 52 46 - $iterator = $file->getFileDataIterator($begin, $end); 47 - foreach ($iterator as $data) { 48 - echo $data; 53 + $file->makeEphemeral(); 54 + 55 + // If we're running in "salvage" mode, wipe out any integrity hash which 56 + // may be present. This makes us read file data without performing an 57 + // integrity check. 58 + $salvage = $args->getArg('salvage'); 59 + if ($salvage) { 60 + $file->setIntegrityHash(null); 61 + } 62 + 63 + try { 64 + $iterator = $file->getFileDataIterator($begin, $end); 65 + foreach ($iterator as $data) { 66 + echo $data; 67 + } 68 + } catch (PhabricatorFileIntegrityException $ex) { 69 + throw new PhutilArgumentUsageException( 70 + pht( 71 + 'File data integrity check failed. Use "--salvage" to bypass '. 72 + 'integrity checks. This flag is dangerous, use it at your own '. 73 + 'risk. Underlying error: %s', 74 + $ex->getMessage())); 49 75 } 50 76 51 77 return 0;
+21 -3
src/applications/files/storage/PhabricatorFile.php
··· 37 37 const METADATA_PARTIAL = 'partial'; 38 38 const METADATA_PROFILE = 'profile'; 39 39 const METADATA_STORAGE = 'storage'; 40 + const METADATA_INTEGRITY = 'integrity'; 40 41 41 42 protected $name; 42 43 protected $mimeType; ··· 324 325 325 326 $data_handle = null; 326 327 $engine_identifier = null; 328 + $integrity_hash = null; 327 329 $exceptions = array(); 328 330 foreach ($engines as $engine) { 329 331 $engine_class = get_class($engine); 330 332 try { 331 - list($engine_identifier, $data_handle) = $file->writeToEngine( 333 + $result = $file->writeToEngine( 332 334 $engine, 333 335 $data, 334 336 $params); 337 + 338 + list($engine_identifier, $data_handle, $integrity_hash) = $result; 335 339 336 340 // We stored the file somewhere so stop trying to write it to other 337 341 // places. ··· 362 366 363 367 $file->setStorageEngine($engine_identifier); 364 368 $file->setStorageHandle($data_handle); 369 + 370 + $file->setIntegrityHash($integrity_hash); 365 371 366 372 $file->readPropertiesFromParameters($params); 367 373 ··· 409 415 'name' => $this->getName(), 410 416 ); 411 417 412 - list($new_identifier, $new_handle) = $this->writeToEngine( 418 + list($new_identifier, $new_handle, $integrity_hash) = $this->writeToEngine( 413 419 $engine, 414 420 $data, 415 421 $params); ··· 420 426 421 427 $this->setStorageEngine($new_identifier); 422 428 $this->setStorageHandle($new_handle); 429 + $this->setIntegrityHash($integrity_hash); 423 430 $this->save(); 424 431 425 432 if (!$make_copy) { ··· 494 501 $formatted_iterator = $format->newWriteIterator($data_iterator); 495 502 $formatted_data = $this->loadDataFromIterator($formatted_iterator); 496 503 504 + $integrity_hash = $engine->newIntegrityHash($formatted_data, $format); 505 + 497 506 $data_handle = $engine->writeFile($formatted_data, $params); 498 507 499 508 if (!$data_handle || strlen($data_handle) > 255) { ··· 518 527 $engine_identifier)); 519 528 } 520 529 521 - return array($engine_identifier, $data_handle); 530 + return array($engine_identifier, $data_handle, $integrity_hash); 522 531 } 523 532 524 533 ··· 1218 1227 public function setIsProfileImage($value) { 1219 1228 $this->metadata[self::METADATA_PROFILE] = $value; 1220 1229 return $this; 1230 + } 1231 + 1232 + public function setIntegrityHash($integrity_hash) { 1233 + $this->metadata[self::METADATA_INTEGRITY] = $integrity_hash; 1234 + return $this; 1235 + } 1236 + 1237 + public function getIntegrityHash() { 1238 + return idx($this->metadata, self::METADATA_INTEGRITY); 1221 1239 } 1222 1240 1223 1241 /**