diff options
Diffstat (limited to 'application/security')
-rw-r--r-- | application/security/BanManager.php | 30 | ||||
-rw-r--r-- | application/security/CookieManager.php | 33 | ||||
-rw-r--r-- | application/security/LoginManager.php | 83 | ||||
-rw-r--r-- | application/security/SessionManager.php | 111 |
4 files changed, 190 insertions, 67 deletions
diff --git a/application/security/BanManager.php b/application/security/BanManager.php index 68190c54..288cbde0 100644 --- a/application/security/BanManager.php +++ b/application/security/BanManager.php | |||
@@ -3,7 +3,8 @@ | |||
3 | 3 | ||
4 | namespace Shaarli\Security; | 4 | namespace Shaarli\Security; |
5 | 5 | ||
6 | use Shaarli\FileUtils; | 6 | use Psr\Log\LoggerInterface; |
7 | use Shaarli\Helper\FileUtils; | ||
7 | 8 | ||
8 | /** | 9 | /** |
9 | * Class BanManager | 10 | * Class BanManager |
@@ -28,8 +29,8 @@ class BanManager | |||
28 | /** @var string Path to the file containing IP bans and failures */ | 29 | /** @var string Path to the file containing IP bans and failures */ |
29 | protected $banFile; | 30 | protected $banFile; |
30 | 31 | ||
31 | /** @var string Path to the log file, used to log bans */ | 32 | /** @var LoggerInterface Path to the log file, used to log bans */ |
32 | protected $logFile; | 33 | protected $logger; |
33 | 34 | ||
34 | /** @var array List of IP with their associated number of failed attempts */ | 35 | /** @var array List of IP with their associated number of failed attempts */ |
35 | protected $failures = []; | 36 | protected $failures = []; |
@@ -40,18 +41,19 @@ class BanManager | |||
40 | /** | 41 | /** |
41 | * BanManager constructor. | 42 | * BanManager constructor. |
42 | * | 43 | * |
43 | * @param array $trustedProxies List of allowed proxies IP | 44 | * @param array $trustedProxies List of allowed proxies IP |
44 | * @param int $nbAttempts Number of allowed failed attempt before the ban | 45 | * @param int $nbAttempts Number of allowed failed attempt before the ban |
45 | * @param int $banDuration Ban duration in seconds | 46 | * @param int $banDuration Ban duration in seconds |
46 | * @param string $banFile Path to the file containing IP bans and failures | 47 | * @param string $banFile Path to the file containing IP bans and failures |
47 | * @param string $logFile Path to the log file, used to log bans | 48 | * @param LoggerInterface $logger PSR-3 logger to save login attempts in log directory |
48 | */ | 49 | */ |
49 | public function __construct($trustedProxies, $nbAttempts, $banDuration, $banFile, $logFile) { | 50 | public function __construct($trustedProxies, $nbAttempts, $banDuration, $banFile, LoggerInterface $logger) { |
50 | $this->trustedProxies = $trustedProxies; | 51 | $this->trustedProxies = $trustedProxies; |
51 | $this->nbAttempts = $nbAttempts; | 52 | $this->nbAttempts = $nbAttempts; |
52 | $this->banDuration = $banDuration; | 53 | $this->banDuration = $banDuration; |
53 | $this->banFile = $banFile; | 54 | $this->banFile = $banFile; |
54 | $this->logFile = $logFile; | 55 | $this->logger = $logger; |
56 | |||
55 | $this->readBanFile(); | 57 | $this->readBanFile(); |
56 | } | 58 | } |
57 | 59 | ||
@@ -78,11 +80,7 @@ class BanManager | |||
78 | 80 | ||
79 | if ($this->failures[$ip] >= $this->nbAttempts) { | 81 | if ($this->failures[$ip] >= $this->nbAttempts) { |
80 | $this->bans[$ip] = time() + $this->banDuration; | 82 | $this->bans[$ip] = time() + $this->banDuration; |
81 | logm( | 83 | $this->logger->info(format_log('IP address banned from login: '. $ip, $ip)); |
82 | $this->logFile, | ||
83 | $server['REMOTE_ADDR'], | ||
84 | 'IP address banned from login: '. $ip | ||
85 | ); | ||
86 | } | 84 | } |
87 | $this->writeBanFile(); | 85 | $this->writeBanFile(); |
88 | } | 86 | } |
@@ -138,7 +136,7 @@ class BanManager | |||
138 | unset($this->failures[$ip]); | 136 | unset($this->failures[$ip]); |
139 | } | 137 | } |
140 | unset($this->bans[$ip]); | 138 | unset($this->bans[$ip]); |
141 | logm($this->logFile, $server['REMOTE_ADDR'], 'Ban lifted for: '. $ip); | 139 | $this->logger->info(format_log('Ban lifted for: '. $ip, $ip)); |
142 | 140 | ||
143 | $this->writeBanFile(); | 141 | $this->writeBanFile(); |
144 | return false; | 142 | return false; |
diff --git a/application/security/CookieManager.php b/application/security/CookieManager.php new file mode 100644 index 00000000..cde4746e --- /dev/null +++ b/application/security/CookieManager.php | |||
@@ -0,0 +1,33 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Shaarli\Security; | ||
6 | |||
7 | class CookieManager | ||
8 | { | ||
9 | /** @var string Name of the cookie set after logging in **/ | ||
10 | public const STAY_SIGNED_IN = 'shaarli_staySignedIn'; | ||
11 | |||
12 | /** @var mixed $_COOKIE set by reference */ | ||
13 | protected $cookies; | ||
14 | |||
15 | public function __construct(array &$cookies) | ||
16 | { | ||
17 | $this->cookies = $cookies; | ||
18 | } | ||
19 | |||
20 | public function setCookieParameter(string $key, string $value, int $expires, string $path): self | ||
21 | { | ||
22 | $this->cookies[$key] = $value; | ||
23 | |||
24 | setcookie($key, $value, $expires, $path); | ||
25 | |||
26 | return $this; | ||
27 | } | ||
28 | |||
29 | public function getCookieParameter(string $key, string $default = null): ?string | ||
30 | { | ||
31 | return $this->cookies[$key] ?? $default; | ||
32 | } | ||
33 | } | ||
diff --git a/application/security/LoginManager.php b/application/security/LoginManager.php index 39ec9b2e..426e785e 100644 --- a/application/security/LoginManager.php +++ b/application/security/LoginManager.php | |||
@@ -2,6 +2,7 @@ | |||
2 | namespace Shaarli\Security; | 2 | namespace Shaarli\Security; |
3 | 3 | ||
4 | use Exception; | 4 | use Exception; |
5 | use Psr\Log\LoggerInterface; | ||
5 | use Shaarli\Config\ConfigManager; | 6 | use Shaarli\Config\ConfigManager; |
6 | 7 | ||
7 | /** | 8 | /** |
@@ -9,9 +10,6 @@ use Shaarli\Config\ConfigManager; | |||
9 | */ | 10 | */ |
10 | class LoginManager | 11 | class LoginManager |
11 | { | 12 | { |
12 | /** @var string Name of the cookie set after logging in **/ | ||
13 | public static $STAY_SIGNED_IN_COOKIE = 'shaarli_staySignedIn'; | ||
14 | |||
15 | /** @var array A reference to the $_GLOBALS array */ | 13 | /** @var array A reference to the $_GLOBALS array */ |
16 | protected $globals = []; | 14 | protected $globals = []; |
17 | 15 | ||
@@ -32,24 +30,32 @@ class LoginManager | |||
32 | 30 | ||
33 | /** @var string User sign-in token depending on remote IP and credentials */ | 31 | /** @var string User sign-in token depending on remote IP and credentials */ |
34 | protected $staySignedInToken = ''; | 32 | protected $staySignedInToken = ''; |
33 | /** @var CookieManager */ | ||
34 | protected $cookieManager; | ||
35 | /** @var LoggerInterface */ | ||
36 | protected $logger; | ||
35 | 37 | ||
36 | /** | 38 | /** |
37 | * Constructor | 39 | * Constructor |
38 | * | 40 | * |
39 | * @param ConfigManager $configManager Configuration Manager instance | 41 | * @param ConfigManager $configManager Configuration Manager instance |
40 | * @param SessionManager $sessionManager SessionManager instance | 42 | * @param SessionManager $sessionManager SessionManager instance |
43 | * @param CookieManager $cookieManager CookieManager instance | ||
44 | * @param BanManager $banManager | ||
45 | * @param LoggerInterface $logger Used to log login attempts | ||
41 | */ | 46 | */ |
42 | public function __construct($configManager, $sessionManager) | 47 | public function __construct( |
43 | { | 48 | ConfigManager $configManager, |
49 | SessionManager $sessionManager, | ||
50 | CookieManager $cookieManager, | ||
51 | BanManager $banManager, | ||
52 | LoggerInterface $logger | ||
53 | ) { | ||
44 | $this->configManager = $configManager; | 54 | $this->configManager = $configManager; |
45 | $this->sessionManager = $sessionManager; | 55 | $this->sessionManager = $sessionManager; |
46 | $this->banManager = new BanManager( | 56 | $this->cookieManager = $cookieManager; |
47 | $this->configManager->get('security.trusted_proxies', []), | 57 | $this->banManager = $banManager; |
48 | $this->configManager->get('security.ban_after'), | 58 | $this->logger = $logger; |
49 | $this->configManager->get('security.ban_duration'), | ||
50 | $this->configManager->get('resource.ban_file', 'data/ipbans.php'), | ||
51 | $this->configManager->get('resource.log') | ||
52 | ); | ||
53 | 59 | ||
54 | if ($this->configManager->get('security.open_shaarli') === true) { | 60 | if ($this->configManager->get('security.open_shaarli') === true) { |
55 | $this->openShaarli = true; | 61 | $this->openShaarli = true; |
@@ -86,10 +92,9 @@ class LoginManager | |||
86 | /** | 92 | /** |
87 | * Check user session state and validity (expiration) | 93 | * Check user session state and validity (expiration) |
88 | * | 94 | * |
89 | * @param array $cookie The $_COOKIE array | ||
90 | * @param string $clientIpId Client IP address identifier | 95 | * @param string $clientIpId Client IP address identifier |
91 | */ | 96 | */ |
92 | public function checkLoginState($cookie, $clientIpId) | 97 | public function checkLoginState($clientIpId) |
93 | { | 98 | { |
94 | if (! $this->configManager->exists('credentials.login')) { | 99 | if (! $this->configManager->exists('credentials.login')) { |
95 | // Shaarli is not configured yet | 100 | // Shaarli is not configured yet |
@@ -97,9 +102,7 @@ class LoginManager | |||
97 | return; | 102 | return; |
98 | } | 103 | } |
99 | 104 | ||
100 | if (isset($cookie[self::$STAY_SIGNED_IN_COOKIE]) | 105 | if ($this->staySignedInToken === $this->cookieManager->getCookieParameter(CookieManager::STAY_SIGNED_IN)) { |
101 | && $cookie[self::$STAY_SIGNED_IN_COOKIE] === $this->staySignedInToken | ||
102 | ) { | ||
103 | // The user client has a valid stay-signed-in cookie | 106 | // The user client has a valid stay-signed-in cookie |
104 | // Session information is updated with the current client information | 107 | // Session information is updated with the current client information |
105 | $this->sessionManager->storeLoginInfo($clientIpId); | 108 | $this->sessionManager->storeLoginInfo($clientIpId); |
@@ -120,7 +123,7 @@ class LoginManager | |||
120 | * | 123 | * |
121 | * @return true when the user is logged in, false otherwise | 124 | * @return true when the user is logged in, false otherwise |
122 | */ | 125 | */ |
123 | public function isLoggedIn() | 126 | public function isLoggedIn(): bool |
124 | { | 127 | { |
125 | if ($this->openShaarli) { | 128 | if ($this->openShaarli) { |
126 | return true; | 129 | return true; |
@@ -131,48 +134,34 @@ class LoginManager | |||
131 | /** | 134 | /** |
132 | * Check user credentials are valid | 135 | * Check user credentials are valid |
133 | * | 136 | * |
134 | * @param string $remoteIp Remote client IP address | ||
135 | * @param string $clientIpId Client IP address identifier | 137 | * @param string $clientIpId Client IP address identifier |
136 | * @param string $login Username | 138 | * @param string $login Username |
137 | * @param string $password Password | 139 | * @param string $password Password |
138 | * | 140 | * |
139 | * @return bool true if the provided credentials are valid, false otherwise | 141 | * @return bool true if the provided credentials are valid, false otherwise |
140 | */ | 142 | */ |
141 | public function checkCredentials($remoteIp, $clientIpId, $login, $password) | 143 | public function checkCredentials($clientIpId, $login, $password) |
142 | { | 144 | { |
143 | // Check login matches config | ||
144 | if ($login !== $this->configManager->get('credentials.login')) { | ||
145 | return false; | ||
146 | } | ||
147 | |||
148 | // Check credentials | 145 | // Check credentials |
149 | try { | 146 | try { |
150 | $useLdapLogin = !empty($this->configManager->get('ldap.host')); | 147 | $useLdapLogin = !empty($this->configManager->get('ldap.host')); |
151 | if ((false === $useLdapLogin && $this->checkCredentialsFromLocalConfig($login, $password)) | 148 | if ($login === $this->configManager->get('credentials.login') |
152 | || (true === $useLdapLogin && $this->checkCredentialsFromLdap($login, $password)) | 149 | && ( |
150 | (false === $useLdapLogin && $this->checkCredentialsFromLocalConfig($login, $password)) | ||
151 | || (true === $useLdapLogin && $this->checkCredentialsFromLdap($login, $password)) | ||
152 | ) | ||
153 | ) { | 153 | ) { |
154 | $this->sessionManager->storeLoginInfo($clientIpId); | 154 | $this->sessionManager->storeLoginInfo($clientIpId); |
155 | logm( | 155 | $this->logger->info(format_log('Login successful', $clientIpId)); |
156 | $this->configManager->get('resource.log'), | 156 | |
157 | $remoteIp, | 157 | return true; |
158 | 'Login successful' | ||
159 | ); | ||
160 | return true; | ||
161 | } | 158 | } |
162 | } | 159 | } catch(Exception $exception) { |
163 | catch(Exception $exception) { | 160 | $this->logger->info(format_log('Exception while checking credentials: ' . $exception, $clientIpId)); |
164 | logm( | ||
165 | $this->configManager->get('resource.log'), | ||
166 | $remoteIp, | ||
167 | 'Exception while checking credentials: ' . $exception | ||
168 | ); | ||
169 | } | 161 | } |
170 | 162 | ||
171 | logm( | 163 | $this->logger->info(format_log('Login failed for user ' . $login, $clientIpId)); |
172 | $this->configManager->get('resource.log'), | 164 | |
173 | $remoteIp, | ||
174 | 'Login failed for user ' . $login | ||
175 | ); | ||
176 | return false; | 165 | return false; |
177 | } | 166 | } |
178 | 167 | ||
diff --git a/application/security/SessionManager.php b/application/security/SessionManager.php index 994fcbe5..96bf193c 100644 --- a/application/security/SessionManager.php +++ b/application/security/SessionManager.php | |||
@@ -8,6 +8,14 @@ use Shaarli\Config\ConfigManager; | |||
8 | */ | 8 | */ |
9 | class SessionManager | 9 | class SessionManager |
10 | { | 10 | { |
11 | public const KEY_LINKS_PER_PAGE = 'LINKS_PER_PAGE'; | ||
12 | public const KEY_VISIBILITY = 'visibility'; | ||
13 | public const KEY_UNTAGGED_ONLY = 'untaggedonly'; | ||
14 | |||
15 | public const KEY_SUCCESS_MESSAGES = 'successes'; | ||
16 | public const KEY_WARNING_MESSAGES = 'warnings'; | ||
17 | public const KEY_ERROR_MESSAGES = 'errors'; | ||
18 | |||
11 | /** @var int Session expiration timeout, in seconds */ | 19 | /** @var int Session expiration timeout, in seconds */ |
12 | public static $SHORT_TIMEOUT = 3600; // 1 hour | 20 | public static $SHORT_TIMEOUT = 3600; // 1 hour |
13 | 21 | ||
@@ -23,16 +31,35 @@ class SessionManager | |||
23 | /** @var bool Whether the user should stay signed in (LONG_TIMEOUT) */ | 31 | /** @var bool Whether the user should stay signed in (LONG_TIMEOUT) */ |
24 | protected $staySignedIn = false; | 32 | protected $staySignedIn = false; |
25 | 33 | ||
34 | /** @var string */ | ||
35 | protected $savePath; | ||
36 | |||
26 | /** | 37 | /** |
27 | * Constructor | 38 | * Constructor |
28 | * | 39 | * |
29 | * @param array $session The $_SESSION array (reference) | 40 | * @param array $session The $_SESSION array (reference) |
30 | * @param ConfigManager $conf ConfigManager instance | 41 | * @param ConfigManager $conf ConfigManager instance |
42 | * @param string $savePath Session save path returned by builtin function session_save_path() | ||
31 | */ | 43 | */ |
32 | public function __construct(& $session, $conf) | 44 | public function __construct(&$session, $conf, string $savePath) |
33 | { | 45 | { |
34 | $this->session = &$session; | 46 | $this->session = &$session; |
35 | $this->conf = $conf; | 47 | $this->conf = $conf; |
48 | $this->savePath = $savePath; | ||
49 | } | ||
50 | |||
51 | /** | ||
52 | * Initialize XSRF token and links per page session variables. | ||
53 | */ | ||
54 | public function initialize(): void | ||
55 | { | ||
56 | if (!isset($this->session['tokens'])) { | ||
57 | $this->session['tokens'] = []; | ||
58 | } | ||
59 | |||
60 | if (!isset($this->session['LINKS_PER_PAGE'])) { | ||
61 | $this->session['LINKS_PER_PAGE'] = $this->conf->get('general.links_per_page', 20); | ||
62 | } | ||
36 | } | 63 | } |
37 | 64 | ||
38 | /** | 65 | /** |
@@ -156,7 +183,6 @@ class SessionManager | |||
156 | unset($this->session['expires_on']); | 183 | unset($this->session['expires_on']); |
157 | unset($this->session['username']); | 184 | unset($this->session['username']); |
158 | unset($this->session['visibility']); | 185 | unset($this->session['visibility']); |
159 | unset($this->session['untaggedonly']); | ||
160 | } | 186 | } |
161 | } | 187 | } |
162 | 188 | ||
@@ -202,4 +228,81 @@ class SessionManager | |||
202 | { | 228 | { |
203 | return $this->session; | 229 | return $this->session; |
204 | } | 230 | } |
231 | |||
232 | /** | ||
233 | * @param mixed $default value which will be returned if the $key is undefined | ||
234 | * | ||
235 | * @return mixed Content stored in session | ||
236 | */ | ||
237 | public function getSessionParameter(string $key, $default = null) | ||
238 | { | ||
239 | return $this->session[$key] ?? $default; | ||
240 | } | ||
241 | |||
242 | /** | ||
243 | * Store a variable in user session. | ||
244 | * | ||
245 | * @param string $key Session key | ||
246 | * @param mixed $value Session value to store | ||
247 | * | ||
248 | * @return $this | ||
249 | */ | ||
250 | public function setSessionParameter(string $key, $value): self | ||
251 | { | ||
252 | $this->session[$key] = $value; | ||
253 | |||
254 | return $this; | ||
255 | } | ||
256 | |||
257 | /** | ||
258 | * Store a variable in user session. | ||
259 | * | ||
260 | * @param string $key Session key | ||
261 | * | ||
262 | * @return $this | ||
263 | */ | ||
264 | public function deleteSessionParameter(string $key): self | ||
265 | { | ||
266 | unset($this->session[$key]); | ||
267 | |||
268 | return $this; | ||
269 | } | ||
270 | |||
271 | public function getSavePath(): string | ||
272 | { | ||
273 | return $this->savePath; | ||
274 | } | ||
275 | |||
276 | /* | ||
277 | * Next public functions wrapping native PHP session API. | ||
278 | */ | ||
279 | |||
280 | public function destroy(): bool | ||
281 | { | ||
282 | $this->session = []; | ||
283 | |||
284 | return session_destroy(); | ||
285 | } | ||
286 | |||
287 | public function start(): bool | ||
288 | { | ||
289 | if (session_status() === PHP_SESSION_ACTIVE) { | ||
290 | $this->destroy(); | ||
291 | } | ||
292 | |||
293 | return session_start(); | ||
294 | } | ||
295 | |||
296 | /** | ||
297 | * Be careful, return type of session_set_cookie_params() changed between PHP 7.1 and 7.2. | ||
298 | */ | ||
299 | public function cookieParameters(int $lifeTime, string $path, string $domain): void | ||
300 | { | ||
301 | session_set_cookie_params($lifeTime, $path, $domain); | ||
302 | } | ||
303 | |||
304 | public function regenerateId(bool $deleteOldSession = false): bool | ||
305 | { | ||
306 | return session_regenerate_id($deleteOldSession); | ||
307 | } | ||
205 | } | 308 | } |