Friendica Communications Platform (please note that this is a clone of the repository at github, issues are handled there) https://friendi.ca
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

461 lines
14 KiB

11 years ago
5 years ago
11 years ago
11 years ago
  1. <?php
  2. /**
  3. * @file include/security.php
  4. */
  5. use Friendica\Core\Addon;
  6. use Friendica\Core\Config;
  7. use Friendica\Core\L10n;
  8. use Friendica\Core\PConfig;
  9. use Friendica\Core\System;
  10. use Friendica\Database\DBA;
  11. use Friendica\Model\Contact;
  12. use Friendica\Model\Group;
  13. use Friendica\Util\DateTimeFormat;
  14. use Friendica\Model\PermissionSet;
  15. /**
  16. * @brief Calculate the hash that is needed for the "Friendica" cookie
  17. *
  18. * @param array $user Record from "user" table
  19. *
  20. * @return string Hashed data
  21. */
  22. function cookie_hash($user)
  23. {
  24. return(hash("sha256", Config::get("system", "site_prvkey") .
  25. $user["prvkey"] .
  26. $user["password"]));
  27. }
  28. /**
  29. * @brief Set the "Friendica" cookie
  30. *
  31. * @param int $time
  32. * @param array $user Record from "user" table
  33. */
  34. function new_cookie($time, $user = [])
  35. {
  36. if ($time != 0) {
  37. $time = $time + time();
  38. }
  39. if ($user) {
  40. $value = json_encode(["uid" => $user["uid"],
  41. "hash" => cookie_hash($user),
  42. "ip" => defaults($_SERVER, 'REMOTE_ADDR', '0.0.0.0')]);
  43. } else {
  44. $value = "";
  45. }
  46. setcookie("Friendica", $value, $time, "/", "", (Config::get('system', 'ssl_policy') == SSL_POLICY_FULL), true);
  47. }
  48. /**
  49. * @brief Sets the provided user's authenticated session
  50. *
  51. * @todo Should be moved to Friendica\Core\Session once it's created
  52. *
  53. * @param type $user_record
  54. * @param type $login_initial
  55. * @param type $interactive
  56. * @param type $login_refresh
  57. */
  58. function authenticate_success($user_record, $login_initial = false, $interactive = false, $login_refresh = false)
  59. {
  60. $a = get_app();
  61. $_SESSION['uid'] = $user_record['uid'];
  62. $_SESSION['theme'] = $user_record['theme'];
  63. $_SESSION['mobile-theme'] = PConfig::get($user_record['uid'], 'system', 'mobile_theme');
  64. $_SESSION['authenticated'] = 1;
  65. $_SESSION['page_flags'] = $user_record['page-flags'];
  66. $_SESSION['my_url'] = System::baseUrl() . '/profile/' . $user_record['nickname'];
  67. $_SESSION['my_address'] = $user_record['nickname'] . '@' . substr(System::baseUrl(), strpos(System::baseUrl(), '://') + 3);
  68. $_SESSION['addr'] = defaults($_SERVER, 'REMOTE_ADDR', '0.0.0.0');
  69. $a->user = $user_record;
  70. if ($interactive) {
  71. if ($a->user['login_date'] <= NULL_DATE) {
  72. $_SESSION['return_url'] = 'profile_photo/new';
  73. $a->module = 'profile_photo';
  74. info(L10n::t("Welcome ") . $a->user['username'] . EOL);
  75. info(L10n::t('Please upload a profile photo.') . EOL);
  76. } else {
  77. info(L10n::t("Welcome back ") . $a->user['username'] . EOL);
  78. }
  79. }
  80. $member_since = strtotime($a->user['register_date']);
  81. if (time() < ($member_since + ( 60 * 60 * 24 * 14))) {
  82. $_SESSION['new_member'] = true;
  83. } else {
  84. $_SESSION['new_member'] = false;
  85. }
  86. if (strlen($a->user['timezone'])) {
  87. date_default_timezone_set($a->user['timezone']);
  88. $a->timezone = $a->user['timezone'];
  89. }
  90. $master_record = $a->user;
  91. if ((x($_SESSION, 'submanage')) && intval($_SESSION['submanage'])) {
  92. $user = DBA::selectFirst('user', [], ['uid' => $_SESSION['submanage']]);
  93. if (DBA::isResult($user)) {
  94. $master_record = $user;
  95. }
  96. }
  97. if ($master_record['parent-uid'] == 0) {
  98. // First add our own entry
  99. $a->identities = [['uid' => $master_record['uid'],
  100. 'username' => $master_record['username'],
  101. 'nickname' => $master_record['nickname']]];
  102. // Then add all the children
  103. $r = DBA::select('user', ['uid', 'username', 'nickname'],
  104. ['parent-uid' => $master_record['uid'], 'account_removed' => false]);
  105. if (DBA::isResult($r)) {
  106. $a->identities = array_merge($a->identities, DBA::toArray($r));
  107. }
  108. } else {
  109. // Just ensure that the array is always defined
  110. $a->identities = [];
  111. // First entry is our parent
  112. $r = DBA::select('user', ['uid', 'username', 'nickname'],
  113. ['uid' => $master_record['parent-uid'], 'account_removed' => false]);
  114. if (DBA::isResult($r)) {
  115. $a->identities = DBA::toArray($r);
  116. }
  117. // Then add all siblings
  118. $r = DBA::select('user', ['uid', 'username', 'nickname'],
  119. ['parent-uid' => $master_record['parent-uid'], 'account_removed' => false]);
  120. if (DBA::isResult($r)) {
  121. $a->identities = array_merge($a->identities, DBA::toArray($r));
  122. }
  123. }
  124. $r = DBA::p("SELECT `user`.`uid`, `user`.`username`, `user`.`nickname`
  125. FROM `manage`
  126. INNER JOIN `user` ON `manage`.`mid` = `user`.`uid`
  127. WHERE `user`.`account_removed` = 0 AND `manage`.`uid` = ?",
  128. $master_record['uid']
  129. );
  130. if (DBA::isResult($r)) {
  131. $a->identities = array_merge($a->identities, DBA::toArray($r));
  132. }
  133. if ($login_initial) {
  134. logger('auth_identities: ' . print_r($a->identities, true), LOGGER_DEBUG);
  135. }
  136. if ($login_refresh) {
  137. logger('auth_identities refresh: ' . print_r($a->identities, true), LOGGER_DEBUG);
  138. }
  139. $contact = DBA::selectFirst('contact', [], ['uid' => $_SESSION['uid'], 'self' => true]);
  140. if (DBA::isResult($contact)) {
  141. $a->contact = $contact;
  142. $a->cid = $contact['id'];
  143. $_SESSION['cid'] = $a->cid;
  144. }
  145. header('X-Account-Management-Status: active; name="' . $a->user['username'] . '"; id="' . $a->user['nickname'] . '"');
  146. if ($login_initial || $login_refresh) {
  147. DBA::update('user', ['login_date' => DateTimeFormat::utcNow()], ['uid' => $_SESSION['uid']]);
  148. // Set the login date for all identities of the user
  149. DBA::update('user', ['login_date' => DateTimeFormat::utcNow()],
  150. ['parent-uid' => $master_record['uid'], 'account_removed' => false]);
  151. }
  152. if ($login_initial) {
  153. /*
  154. * If the user specified to remember the authentication, then set a cookie
  155. * that expires after one week (the default is when the browser is closed).
  156. * The cookie will be renewed automatically.
  157. * The week ensures that sessions will expire after some inactivity.
  158. */
  159. if ($_SESSION['remember']) {
  160. logger('Injecting cookie for remembered user ' . $a->user['nickname']);
  161. new_cookie(604800, $user_record);
  162. unset($_SESSION['remember']);
  163. }
  164. }
  165. if ($login_initial) {
  166. Addon::callHooks('logged_in', $a->user);
  167. if (($a->module !== 'home') && isset($_SESSION['return_url'])) {
  168. goaway(System::baseUrl() . '/' . $_SESSION['return_url']);
  169. }
  170. }
  171. }
  172. function can_write_wall($owner)
  173. {
  174. static $verified = 0;
  175. if (!local_user() && !remote_user()) {
  176. return false;
  177. }
  178. $uid = local_user();
  179. if ($uid == $owner) {
  180. return true;
  181. }
  182. if (local_user() && ($owner == 0)) {
  183. return true;
  184. }
  185. if (remote_user()) {
  186. // use remembered decision and avoid a DB lookup for each and every display item
  187. // DO NOT use this function if there are going to be multiple owners
  188. // We have a contact-id for an authenticated remote user, this block determines if the contact
  189. // belongs to this page owner, and has the necessary permissions to post content
  190. if ($verified === 2) {
  191. return true;
  192. } elseif ($verified === 1) {
  193. return false;
  194. } else {
  195. $cid = 0;
  196. if (is_array($_SESSION['remote'])) {
  197. foreach ($_SESSION['remote'] as $visitor) {
  198. if ($visitor['uid'] == $owner) {
  199. $cid = $visitor['cid'];
  200. break;
  201. }
  202. }
  203. }
  204. if (!$cid) {
  205. return false;
  206. }
  207. $r = q("SELECT `contact`.*, `user`.`page-flags` FROM `contact` INNER JOIN `user` on `user`.`uid` = `contact`.`uid`
  208. WHERE `contact`.`uid` = %d AND `contact`.`id` = %d AND `contact`.`blocked` = 0 AND `contact`.`pending` = 0
  209. AND `user`.`blockwall` = 0 AND `readonly` = 0 AND ( `contact`.`rel` IN ( %d , %d ) OR `user`.`page-flags` = %d ) LIMIT 1",
  210. intval($owner),
  211. intval($cid),
  212. intval(Contact::SHARING),
  213. intval(Contact::FRIEND),
  214. intval(Contact::PAGE_COMMUNITY)
  215. );
  216. if (DBA::isResult($r)) {
  217. $verified = 2;
  218. return true;
  219. } else {
  220. $verified = 1;
  221. }
  222. }
  223. }
  224. return false;
  225. }
  226. /// @TODO $groups should be array
  227. function permissions_sql($owner_id, $remote_verified = false, $groups = null)
  228. {
  229. $local_user = local_user();
  230. $remote_user = remote_user();
  231. /**
  232. * Construct permissions
  233. *
  234. * default permissions - anonymous user
  235. */
  236. $sql = " AND allow_cid = ''
  237. AND allow_gid = ''
  238. AND deny_cid = ''
  239. AND deny_gid = ''
  240. ";
  241. /**
  242. * Profile owner - everything is visible
  243. */
  244. if ($local_user && $local_user == $owner_id) {
  245. $sql = '';
  246. /**
  247. * Authenticated visitor. Unless pre-verified,
  248. * check that the contact belongs to this $owner_id
  249. * and load the groups the visitor belongs to.
  250. * If pre-verified, the caller is expected to have already
  251. * done this and passed the groups into this function.
  252. */
  253. } elseif ($remote_user) {
  254. /*
  255. * Authenticated visitor. Unless pre-verified,
  256. * check that the contact belongs to this $owner_id
  257. * and load the groups the visitor belongs to.
  258. * If pre-verified, the caller is expected to have already
  259. * done this and passed the groups into this function.
  260. */
  261. if (!$remote_verified) {
  262. $r = q("SELECT id FROM contact WHERE id = %d AND uid = %d AND blocked = 0 LIMIT 1",
  263. intval($remote_user),
  264. intval($owner_id)
  265. );
  266. if (DBA::isResult($r)) {
  267. $remote_verified = true;
  268. $groups = Group::getIdsByContactId($remote_user);
  269. }
  270. }
  271. if ($remote_verified) {
  272. $gs = '<<>>'; // should be impossible to match
  273. if (is_array($groups)) {
  274. foreach ($groups as $g) {
  275. $gs .= '|<' . intval($g) . '>';
  276. }
  277. }
  278. $sql = sprintf(
  279. " AND ( NOT (deny_cid REGEXP '<%d>' OR deny_gid REGEXP '%s')
  280. AND ( allow_cid REGEXP '<%d>' OR allow_gid REGEXP '%s' OR ( allow_cid = '' AND allow_gid = '') )
  281. )
  282. ",
  283. intval($remote_user),
  284. DBA::escape($gs),
  285. intval($remote_user),
  286. DBA::escape($gs)
  287. );
  288. }
  289. }
  290. return $sql;
  291. }
  292. function item_permissions_sql($owner_id, $remote_verified = false, $groups = null)
  293. {
  294. $local_user = local_user();
  295. $remote_user = remote_user();
  296. /*
  297. * Construct permissions
  298. *
  299. * default permissions - anonymous user
  300. */
  301. $sql = " AND NOT `item`.`private`";
  302. // Profile owner - everything is visible
  303. if ($local_user && ($local_user == $owner_id)) {
  304. $sql = '';
  305. } elseif ($remote_user) {
  306. /*
  307. * Authenticated visitor. Unless pre-verified,
  308. * check that the contact belongs to this $owner_id
  309. * and load the groups the visitor belongs to.
  310. * If pre-verified, the caller is expected to have already
  311. * done this and passed the groups into this function.
  312. */
  313. $set = PermissionSet::get($owner_id, $remote_user, $groups);
  314. if (!empty($set)) {
  315. $sql_set = " OR (`item`.`private` IN (1,2) AND `item`.`wall` AND `item`.`psid` IN (" . implode(',', $set) . "))";
  316. } else {
  317. $sql_set = '';
  318. }
  319. $sql = " AND (NOT `item`.`private`" . $sql_set . ")";
  320. }
  321. return $sql;
  322. }
  323. /*
  324. * Functions used to protect against Cross-Site Request Forgery
  325. * The security token has to base on at least one value that an attacker can't know - here it's the session ID and the private key.
  326. * In this implementation, a security token is reusable (if the user submits a form, goes back and resubmits the form, maybe with small changes;
  327. * or if the security token is used for ajax-calls that happen several times), but only valid for a certain amout of time (3hours).
  328. * The "typename" seperates the security tokens of different types of forms. This could be relevant in the following case:
  329. * A security token is used to protekt a link from CSRF (e.g. the "delete this profile"-link).
  330. * If the new page contains by any chance external elements, then the used security token is exposed by the referrer.
  331. * Actually, important actions should not be triggered by Links / GET-Requests at all, but somethimes they still are,
  332. * so this mechanism brings in some damage control (the attacker would be able to forge a request to a form of this type, but not to forms of other types).
  333. */
  334. function get_form_security_token($typename = '')
  335. {
  336. $a = get_app();
  337. $timestamp = time();
  338. $sec_hash = hash('whirlpool', $a->user['guid'] . $a->user['prvkey'] . session_id() . $timestamp . $typename);
  339. return $timestamp . '.' . $sec_hash;
  340. }
  341. function check_form_security_token($typename = '', $formname = 'form_security_token')
  342. {
  343. $hash = null;
  344. if (!empty($_REQUEST[$formname])) {
  345. /// @TODO Careful, not secured!
  346. $hash = $_REQUEST[$formname];
  347. }
  348. if (!empty($_SERVER['HTTP_X_CSRF_TOKEN'])) {
  349. /// @TODO Careful, not secured!
  350. $hash = $_SERVER['HTTP_X_CSRF_TOKEN'];
  351. }
  352. if (empty($hash)) {
  353. return false;
  354. }
  355. $max_livetime = 10800; // 3 hours
  356. $a = get_app();
  357. $x = explode('.', $hash);
  358. if (time() > (IntVal($x[0]) + $max_livetime)) {
  359. return false;
  360. }
  361. $sec_hash = hash('whirlpool', $a->user['guid'] . $a->user['prvkey'] . session_id() . $x[0] . $typename);
  362. return ($sec_hash == $x[1]);
  363. }
  364. function check_form_security_std_err_msg()
  365. {
  366. return L10n::t("The form security token was not correct. This probably happened because the form has been opened for too long \x28>3 hours\x29 before submitting it.") . EOL;
  367. }
  368. function check_form_security_token_redirectOnErr($err_redirect, $typename = '', $formname = 'form_security_token')
  369. {
  370. if (!check_form_security_token($typename, $formname)) {
  371. $a = get_app();
  372. logger('check_form_security_token failed: user ' . $a->user['guid'] . ' - form element ' . $typename);
  373. logger('check_form_security_token failed: _REQUEST data: ' . print_r($_REQUEST, true), LOGGER_DATA);
  374. notice(check_form_security_std_err_msg());
  375. goaway(System::baseUrl() . $err_redirect);
  376. }
  377. }
  378. function check_form_security_token_ForbiddenOnErr($typename = '', $formname = 'form_security_token')
  379. {
  380. if (!check_form_security_token($typename, $formname)) {
  381. $a = get_app();
  382. logger('check_form_security_token failed: user ' . $a->user['guid'] . ' - form element ' . $typename);
  383. logger('check_form_security_token failed: _REQUEST data: ' . print_r($_REQUEST, true), LOGGER_DATA);
  384. header('HTTP/1.1 403 Forbidden');
  385. killme();
  386. }
  387. }
  388. /**
  389. * @brief Kills the "Friendica" cookie and all session data
  390. */
  391. function nuke_session()
  392. {
  393. new_cookie(-3600); // make sure cookie is deleted on browser close, as a security measure
  394. session_unset();
  395. session_destroy();
  396. }