Skip to content

Commit

Permalink
[!!!][FEATURE] Add IP locking for IPv6
Browse files Browse the repository at this point in the history
A new IpLocker class replaces the lock functionality in
AbstractUserAuthentication.

The new class is capable of locking to IPv4 and IPv6 addresses.

Resolves: #21638
Releases: master
Change-Id: I0075a9e49690e31c938abf7242c1f088c73bb37d
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/52947
Reviewed-by: Benni Mack <benni@typo3.org>
Reviewed-by: Stefan Neufeind <typo3.neufeind@speedpartner.de>
Reviewed-by: Markus Klein <markus.klein@typo3.org>
Reviewed-by: Oliver Hader <oliver.hader@typo3.org>
Tested-by: TYPO3com <noreply@typo3.com>
Tested-by: Markus Klein <markus.klein@typo3.org>
Tested-by: Oliver Hader <oliver.hader@typo3.org>
  • Loading branch information
astehlik authored and ohader committed Jul 18, 2019
1 parent b9fb564 commit 399bf83
Show file tree
Hide file tree
Showing 10 changed files with 384 additions and 41 deletions.
Expand Up @@ -32,7 +32,6 @@
use TYPO3\CMS\Core\Session\Backend\SessionBackendInterface;
use TYPO3\CMS\Core\Session\SessionManager;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;

/**
* Authentication of users in TYPO3
Expand Down Expand Up @@ -201,13 +200,6 @@ abstract class AbstractUserAuthentication implements LoggerAwareInterface
*/
public $hash_length = 32;

/**
* If set to 4, the session will be locked to the user's IP address (all four numbers).
* Reducing this to 1-3 means that only the given number of parts of the IP address is used.
* @var int
*/
public $lockIP = 4;

/**
* @var string
*/
Expand Down Expand Up @@ -302,6 +294,11 @@ abstract class AbstractUserAuthentication implements LoggerAwareInterface
*/
public $uc;

/**
* @var IpLocker
*/
protected $ipLocker;

/**
* @var SessionBackendInterface
*/
Expand Down Expand Up @@ -331,6 +328,12 @@ public function __construct()
if (empty($this->loginType)) {
throw new Exception('No loginType defined, must be set explicitly by subclass', 1476045345);
}

$this->ipLocker = GeneralUtility::makeInstance(
IpLocker::class,
$GLOBALS['TYPO3_CONF_VARS'][$this->loginType]['lockIP'],
$GLOBALS['TYPO3_CONF_VARS'][$this->loginType]['lockIPv6']
);
}

/**
Expand Down Expand Up @@ -893,10 +896,7 @@ protected function updateLoginTimestamp(int $userId)
*/
public function getNewSessionRecord($tempuser)
{
$sessionIpLock = '[DISABLED]';
if ($this->lockIP && empty($tempuser['disableIPlock'])) {
$sessionIpLock = $this->ipLockClause_remoteIPNumber($this->lockIP);
}
$sessionIpLock = $this->ipLocker->getSessionIpLock((string)GeneralUtility::getIndpEnv('REMOTE_ADDR'), empty($tempuser['disableIPlock']));

return [
'ses_id' => $this->id,
Expand Down Expand Up @@ -1041,10 +1041,7 @@ public function isExistingSessionRecord($id)
}
// If the session does not match the current IP lock, it should be treated as invalid
// and a new session should be created.
if ($sessionRecord['ses_iplock'] !== $this->ipLockClause_remoteIPNumber($this->lockIP) && $sessionRecord['ses_iplock'] !== '[DISABLED]') {
return false;
}
return true;
return $this->ipLocker->validateRemoteAddressAgainstSessionIpLock((string)GeneralUtility::getIndpEnv('REMOTE_ADDR'), $sessionRecord['ses_iplock']);
} catch (SessionNotFoundException $e) {
return false;
}
Expand Down Expand Up @@ -1101,27 +1098,6 @@ protected function userConstraints(): QueryRestrictionContainerInterface
return $restrictionContainer;
}

/**
* Returns the IP address to lock to.
* The IP address may be partial based on $parts.
*
* @param int $parts 1-4: Indicates how many parts of the IP address to return. 4 means all, 1 means only first number.
* @return string (Partial) IP address for REMOTE_ADDR
*/
protected function ipLockClause_remoteIPNumber($parts)
{
$IP = GeneralUtility::getIndpEnv('REMOTE_ADDR');
if ($parts >= 4) {
return $IP;
}
$parts = MathUtility::forceIntegerInRange($parts, 1, 3);
$IPparts = explode('.', $IP);
for ($a = 4; $a > $parts; $a--) {
unset($IPparts[$a - 1]);
}
return implode('.', $IPparts);
}

/*************************
*
* Session and Configuration Handling
Expand Down
113 changes: 113 additions & 0 deletions typo3/sysext/core/Classes/Authentication/IpLocker.php
@@ -0,0 +1,113 @@
<?php
declare(strict_types = 1);

namespace TYPO3\CMS\Core\Authentication;

/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/

use TYPO3\CMS\Core\Utility\MathUtility;

/**
* Handles the locking of sessions to IP addresses.
*/
class IpLocker
{
const DISABLED_LOCK_VALUE = '[DISABLED]';

/**
* If set to 4, the session will be locked to the user's IP address (all four numbers).
* Reducing this to 1-3 means that only the given number of parts of the IP address is used.
*
* @var int
*/
protected $lockIPv4PartCount = 4;

/**
* Same setting as lockIP but for IPv6 addresses.
*
* @var int
*/
protected $lockIPv6PartCount = 8;

public function __construct(int $lockIPv4PartCount, int $lockIPv6PartCount)
{
$this->lockIPv4PartCount = $lockIPv4PartCount;
$this->lockIPv6PartCount = $lockIPv6PartCount;
}

public function getSessionIpLock(string $ipAddress, bool $enableLocking = true): string
{
if (!$enableLocking) {
return static::DISABLED_LOCK_VALUE;
}

if ($this->isIpv6Address($ipAddress)) {
return $this->getIpLockPartForIpv6Address($ipAddress);
}
return $this->getIpLockPartForIpv4Address($ipAddress);
}

public function validateRemoteAddressAgainstSessionIpLock(string $ipAddress, string $sessionIpLock): bool
{
if ($sessionIpLock === static::DISABLED_LOCK_VALUE) {
return true;
}

$ipToCompare = $this->isIpv6Address($sessionIpLock)
? $this->getIpLockPartForIpv6Address($ipAddress)
: $this->getIpLockPartForIpv4Address($ipAddress);
return $ipToCompare === $sessionIpLock;
}

protected function getIpLockPart(string $ipAddress, int $numberOfParts, int $maxParts, string $delimiter): string
{
if ($numberOfParts >= $maxParts) {
return $ipAddress;
}

$numberOfParts = MathUtility::forceIntegerInRange($numberOfParts, 1, $maxParts);
$ipParts = explode($delimiter, $ipAddress);
for ($a = $maxParts; $a > $numberOfParts; $a--) {
$ipPartValue = $delimiter === '.' ? '0' : str_pad('', strlen($ipParts[$a - 1]), '0');
$ipParts[$a - 1] = $ipPartValue;
}

return implode($delimiter, $ipParts);
}

protected function getIpLockPartForIpv4Address(string $ipAddress): string
{
if ($this->lockIPv4PartCount === 0) {
return static::DISABLED_LOCK_VALUE;
}

return $this->getIpLockPart($ipAddress, $this->lockIPv4PartCount, 4, '.');
}

protected function getIpLockPartForIpv6Address(string $ipAddress): string
{
if ($this->lockIPv6PartCount === 0) {
return static::DISABLED_LOCK_VALUE;
}

// inet_pton also takes care of IPv4-mapped addresses (see https://en.wikipedia.org/wiki/IPv6_address#Representation)
$expandedAddress = rtrim(chunk_split(unpack('H*hex', inet_pton($ipAddress))['hex'], 4, ':'), ':');
return $this->getIpLockPart($expandedAddress, $this->lockIPv6PartCount, 8, ':');
}

protected function isIpv6Address(string $ipAddress): bool
{
return strpos($ipAddress, ':') !== false;
}
}
2 changes: 2 additions & 0 deletions typo3/sysext/core/Configuration/DefaultConfiguration.php
Expand Up @@ -1067,6 +1067,7 @@
'warning_email_addr' => '',
'warning_mode' => 0,
'lockIP' => 4,
'lockIPv6' => 2,
'sessionTimeout' => 28800, // a backend user logged in for 8 hours
'IPmaskList' => '',
'lockBeUserToDBmounts' => true,
Expand Down Expand Up @@ -1247,6 +1248,7 @@
'addRootLineFields' => '',
'checkFeUserPid' => true,
'lockIP' => 2,
'lockIPv6' => 2,
'loginSecurityLevel' => 'normal',
'lifetime' => 0,
'sessionTimeout' => 6000,
Expand Down
Expand Up @@ -273,8 +273,21 @@ BE:
'1': 'Use the first part of the editors'' IPv4 address (e.g. "192.") as part of the session locking of Backend Users'
'2': 'Use the first two parts of the editors'' IPv4 address (e.g. "192.168") as part of the session locking of Backend Users'
'3': 'Use the first three parts of the editors'' IPv4 address (e.g. "192.168.13") as part of the session locking of Backend Users'
'4': 'Default: Use the full editors'' IPv4 address (e.g. "192.168.13.84") as part of the session locking of Backend Users (highest security)'
'4': 'Default: Use the editors'' full IPv4 address (e.g. "192.168.13.84") as part of the session locking of Backend Users (highest security)'
description: 'Session IP locking for backend users. See <a href="#FE-lockIP">[FE][lockIP]</a> for details.'
lockIPv6:
type: int
allowedValues:
'0': 'Do not lock Backend User sessions to their IP address at all'
'1': 'Use the first block (16 bits) of the editors'' IPv6 address (e.g. "2001:") as part of the session locking of Backend Users'
'2': 'Use the first two blocks (32 bits) of the editors'' IPv6 address (e.g. "2001:0db8") as part of the session locking of Backend Users'
'3': 'Use the first three blocks (48 bits) of the editors'' IPv6 address (e.g. "2001:0db8:85a3") as part of the session locking of Backend Users'
'4': 'Use the first four blocks (64 bits) of the editors'' IPv6 address (e.g. "2001:0db8:85a3:08d3") as part of the session locking of Backend Users'
'5': 'Use the first five blocks (80 bits) of the editors'' IPv6 address (e.g. "2001:0db8:85a3:08d3:1319") as part of the session locking of Backend Users'
'6': 'Use the first six blocks (96 bits) of the editors'' IPv6 address (e.g. "2001:0db8:85a3:08d3:1319:8a2e") as part of the session locking of Backend Users'
'7': 'Use the first seven blocks (112 bits) of the editors'' IPv6 address (e.g. "2001:0db8:85a3:08d3:1319:8a2e:0370") as part of the session locking of Backend Users'
'8': 'Default: Use the editors'' full IPv6 address (e.g. "2001:0db8:85a3:08d3:1319:8a2e:0370:7344") as part of the session locking of Backend Users (highest security)'
description: 'Session IPv6 locking for backend users. See <a href="#FE-lockIPv6">[FE][lockIPv6]</a> for details.'
sessionTimeout:
type: int
description: 'Session time out for backend users in seconds. The value must be at least 180 to avoid side effects. Default is 28.800 seconds = 8 hours.'
Expand Down Expand Up @@ -396,8 +409,21 @@ FE:
'1': 'Use the first part of the visitors'' IPv4 address (e.g. "192.") as part of the session locking of Frontend Users'
'2': 'Default - Use the first two parts of the visitors'' IPv4 address (e.g. "192.168") as part of the session locking of Frontend Users'
'3': 'Use the first three parts of the visitors'' IPv4 address (e.g. "192.168.13") as part of the session locking of Frontend Users'
'4': 'Use the full visitors'' IPv4 address (e.g. "192.168.13.84") as part of the session locking of Frontend Users (highest security)'
description: 'If activated, Frontend Users are locked to (a part of) their public IP (<code>$_SERVER[''REMOTE_ADDR'']</code>) for their session. Enhances security but may throw off users that may change IP during their session (in which case you can lower it to 2 or 3). The integer indicates how many parts of the IP address to include in the check for session (next to the user agent)..'
'4': 'Use the visitors'' full IPv4 address (e.g. "192.168.13.84") as part of the session locking of Frontend Users (highest security)'
description: 'If activated, Frontend Users are locked to (a part of) their public IP (<code>$_SERVER[''REMOTE_ADDR'']</code>) for their session, if REMOTE_ADDR is an IPv4-address. Enhances security but may throw off users that may change IP during their session (in which case you can lower it). The integer indicates how many parts of the IP address to include in the check for session (next to the user agent).'
lockIPv6:
type: int
allowedValues:
'0': 'Do not lock Backend User sessions to their IP address at all'
'1': 'Use the first block (16 bits) of the editors'' IPv6 address (e.g. "2001:") as part of the session locking of Backend Users'
'2': 'Use the first two blocks (32 bits) of the editors'' IPv6 address (e.g. "2001:0db8") as part of the session locking of Backend Users'
'3': 'Use the first three blocks (48 bits) of the editors'' IPv6 address (e.g. "2001:0db8:85a3") as part of the session locking of Backend Users'
'4': 'Use the first four blocks (64 bits) of the editors'' IPv6 address (e.g. "2001:0db8:85a3:08d3") as part of the session locking of Backend Users'
'5': 'Use the first five blocks (80 bits) of the editors'' IPv6 address (e.g. "2001:0db8:85a3:08d3:1319") as part of the session locking of Backend Users'
'6': 'Use the first six blocks (96 bits) of the editors'' IPv6 address (e.g. "2001:0db8:85a3:08d3:1319:8a2e") as part of the session locking of Backend Users'
'7': 'Use the first seven blocks (112 bits) of the editors'' IPv6 address (e.g. "2001:0db8:85a3:08d3:1319:8a2e:0370") as part of the session locking of Backend Users'
'8': 'Default: Use the visitors'' full IPv6 address (e.g. "2001:0db8:85a3:08d3:1319:8a2e:0370:7344") as part of the session locking of Backend Users (highest security)'
description: 'If activated, Frontend Users are locked to (a part of) their public IP (<code>$_SERVER[''REMOTE_ADDR'']</code>) for their session, if REMOTE_ADDR is an IPv6-address. Enhances security but may throw off users that may change IP during their session (in which case you can lower it). The integer indicates how many parts of the IP address to include in the check for session (next to the user agent).'
loginSecurityLevel:
type: text
description: 'See description for <a href="#BE-loginSecurityLevel">[BE][loginSecurityLevel]</a>. Default state for frontend is "normal". The client/server communication should be secured with HTTPS.'
Expand Down
@@ -0,0 +1,36 @@
.. include:: ../../Includes.txt

======================================================================
Breaking: #21638 - AbstractUserAuthentication::lockIP property removed
======================================================================

See :issue:`21638`

Description
===========

The IP-locking-functionality is extended from IPv4 only to now also support IPv6. A separate IpLocker-functionality was added.

The public property :php:`lockIP` in :php:`AbstractUserAuthentication` is now removed. It usually shouldn't have been accessed directly and supported IPv4 only.


Impact
======

Extensions relying on :php:`lockIP` won't be able to perform their task anymore.
This might for example be the case when "lockIP" was set dynamically, depending on the REMOTE_ADDR.


Affected Installations
======================

Every 3rd party extension depending on the formerly public :php:`lockIP` property is affected.


Migration
=========

Set :php:`lockIP` and :php:`lockIPv6` in :php:`TYPO3_CONF_VARS` - for FE or BE depending on the usecase.
Use the new :php:`\TYPO3\CMS\Core\Authentication\IpLocker` API.

.. index:: Backend, Frontend, LocalConfiguration, NotScanned
@@ -0,0 +1,30 @@
.. include:: ../../Includes.txt

================================================
Feature: #21638 - Introduced IP locking for IPv6
================================================

See :issue:`21638`

Description
===========

The IP-locking functionality has been extended to support IPv6 now as well.
This security feature enables binding of a user session (Backend or Frontend) to an IP address or a part of it.

The available configuration options with their default values for IP-locking are:

.. code-block:: typoscript
$GLOBALS['TYPO3_CONF_VARS']['FE']['lockIP'] = 2;
$GLOBALS['TYPO3_CONF_VARS']['FE']['lockIPv6'] = 2;
$GLOBALS['TYPO3_CONF_VARS']['BE']['lockIP'] = 4;
$GLOBALS['TYPO3_CONF_VARS']['BE']['lockIPv6'] = 2;
The configuration can be changed via the Admin Tools -> Settings menu.
The exact meaning of the numbers used for the configuration are documented there.

Code-wise a separate IpLocker class :php:`\TYPO3\CMS\Core\Authentication\IpLocker` has been added, which takes care of the IP-locking for both IP versions.

.. index:: Backend, Frontend, LocalConfiguration
Expand Up @@ -52,6 +52,7 @@ protected function setUp(): void
$GLOBALS['TYPO3_CONF_VARS']['BE']['cookieName'] = 'be_typo_user';
$GLOBALS['TYPO3_CONF_VARS']['BE']['warning_email_addr'] = '';
$GLOBALS['TYPO3_CONF_VARS']['BE']['lockIP'] = 4;
$GLOBALS['TYPO3_CONF_VARS']['BE']['lockIPv6'] = 8;
$GLOBALS['TYPO3_CONF_VARS']['BE']['sessionTimeout'] = 28800;

$this->subject = new BackendUserAuthentication();
Expand Down
Expand Up @@ -59,6 +59,12 @@ class BackendUserAuthenticationTest extends UnitTestCase
'recursivedeleteFolder' => false
];

protected function setUp(): void
{
$GLOBALS['TYPO3_CONF_VARS']['BE']['lockIP'] = 4;
$GLOBALS['TYPO3_CONF_VARS']['BE']['lockIPv6'] = 8;
}

/**
* Tear down
*/
Expand Down

0 comments on commit 399bf83

Please sign in to comment.