1<?php 2 3/** 4 * Tests for the ACL enforcement around media uploads. 5 * 6 * media_save() trusts the authorization level supplied by its caller so that 7 * delegated saves (for example the bureaucracy plugin's "runas" uploads) keep 8 * working. The public upload endpoints media_upload() and media_upload_xhr() 9 * must therefore authorize the resolved target id themselves, because the 10 * requested media id may contain namespace separators pointing into a 11 * namespace other than the one the upload form was opened for. 12 */ 13class media_upload_test extends DokuWikiTest { 14 15 public function setUp(): void { 16 parent::setUp(); 17 18 global $conf, $AUTH_ACL, $USERINFO, $MSG; 19 20 $conf['useacl'] = 1; 21 $_SERVER['REMOTE_USER'] = 'john'; 22 $USERINFO['grps'] = ['user']; 23 $MSG = []; 24 25 // upload allowed in public:* and public:sub:*, but denied in the 26 // sibling child namespace public:private:* 27 $AUTH_ACL = [ 28 '* @ALL 0', 29 'public:* @user 8', // AUTH_UPLOAD 30 'public:sub:* @user 8', // AUTH_UPLOAD 31 'public:private:* @user 0', // AUTH_NONE 32 ]; 33 } 34 35 /** 36 * Provide a fresh temporary copy of a valid PNG. 37 * 38 * @return string path to the temporary file 39 */ 40 protected function tmpImage() { 41 global $conf; 42 $orig = mediaFN('wiki:dokuwiki-128.png'); 43 $tmp = $conf['tmpdir'] . '/media_upload_test_' . md5($orig . microtime()) . '.png'; 44 copy($orig, $tmp); 45 return $tmp; 46 } 47 48 /** 49 * Simulate a media_upload() request for the given namespace and media id. 50 * 51 * @param string $ns the namespace the upload form was opened for 52 * @param string $mediaid the requested (possibly nested) media id 53 * @return false|string the media_upload() return value 54 */ 55 protected function upload($ns, $mediaid) { 56 global $MSG; 57 $MSG = []; 58 59 $_POST['mediaid'] = $mediaid; 60 $_POST['ow'] = ''; 61 $_REQUEST['sectok'] = getSecurityToken(); 62 63 $file = [ 64 'name' => noNS($mediaid), 65 'tmp_name' => $this->tmpImage(), 66 'error' => 0, 67 'size' => 100, 68 ]; 69 70 return media_upload($ns, AUTH_UPLOAD, $file); 71 } 72 73 /** 74 * Collect the messages emitted during the last request. 75 * 76 * @return string[] 77 */ 78 protected function messages() { 79 global $MSG; 80 return array_map(static fn($m) => $m['msg'], $MSG ?? []); 81 } 82 83 /** 84 * A nested media id must be authorized against its own namespace, so an 85 * upload into an ACL-denied child namespace is rejected even though the 86 * form was opened for a permitted parent namespace. 87 */ 88 public function test_upload_rejects_denied_child_namespace() { 89 $res = $this->upload('public', 'private:target.png'); 90 91 $this->assertFalse($res); 92 $this->assertContains( 93 "You don't have permissions to upload files.", 94 $this->messages(), 95 'the upload must be denied by the ACL check' 96 ); 97 $this->assertFileDoesNotExist(mediaFN('public:private:target.png')); 98 } 99 100 /** 101 * A nested media id into a permitted namespace must pass the ACL check. 102 * 103 * The actual move fails in the test environment (is_uploaded_file()), so we 104 * only assert that the request got past authorization. 105 */ 106 public function test_upload_allows_permitted_child_namespace() { 107 $res = $this->upload('public', 'sub:target.png'); 108 109 $this->assertFalse($res); 110 $this->assertNotContains( 111 "You don't have permissions to upload files.", 112 $this->messages(), 113 'the upload must pass the ACL check for a permitted target' 114 ); 115 } 116 117 /** 118 * media_save() must trust the authorization level supplied by the caller, 119 * so a privileged (delegated) save succeeds even when the current session 120 * user has no access to the target namespace. 121 */ 122 public function test_save_trusts_supplied_auth() { 123 $id = 'public:private:delegated.png'; 124 $res = media_save( 125 ['name' => $this->tmpImage(), 'mime' => 'image/png', 'ext' => 'png'], 126 $id, 127 false, 128 AUTH_UPLOAD, 129 'copy' 130 ); 131 132 $this->assertSame($id, $res); 133 $this->assertFileExists(mediaFN($id)); 134 } 135 136 /** 137 * media_save() must still reject a caller that supplies an insufficient 138 * authorization level. 139 */ 140 public function test_save_enforces_supplied_auth() { 141 $id = 'public:target.png'; 142 $res = media_save( 143 ['name' => $this->tmpImage(), 'mime' => 'image/png', 'ext' => 'png'], 144 $id, 145 false, 146 AUTH_READ, 147 'copy' 148 ); 149 150 $this->assertIsArray($res); 151 $this->assertSame(-1, $res[1]); 152 $this->assertFileDoesNotExist(mediaFN($id)); 153 } 154} 155