]> BookStack Code Mirror - bookstack/commitdiff
Added ability to adjust stored IP address precision
authorDan Brown <redacted>
Sat, 23 Jul 2022 12:41:29 +0000 (13:41 +0100)
committerDan Brown <redacted>
Sat, 23 Jul 2022 12:41:29 +0000 (13:41 +0100)
Included tests to cover.

For #3560

.env.example.complete
app/Actions/ActivityLogger.php
app/Actions/IpFormatter.php [new file with mode: 0644]
app/Config/app.php
phpunit.xml
tests/Actions/AuditLogTest.php
tests/Unit/IpFormatterTest.php [new file with mode: 0644]

index c40ab1380ffd82f63294bd7493e47436d11acb20..1e2160d07a4f1802e13ec6cc13a207989555af24 100644 (file)
@@ -357,3 +357,11 @@ API_REQUESTS_PER_MIN=180
 # user identifier (Username or email).
 LOG_FAILED_LOGIN_MESSAGE=false
 LOG_FAILED_LOGIN_CHANNEL=errorlog_plain_webserver
+
+# Alter the precision of IP addresses stored by BookStack.
+# Should be a number between 0 and 4, where 4 retains the full IP address
+# and 0 completely hides the IP address. As an examples, a value of 2 for the
+# IP address '146.191.42.4' would result in '146.191.x.x' being logged.
+# For the IP address '2001:db8:85a3:8d3:1319:8a2e:370:7348' this would result as:
+# '2001:db8:85a3:8d3:x:x:x:x'
+IP_ADDRESS_PRECISION=4
\ No newline at end of file
index 468bb47055c995a9faaab7535e5451cf11dcf4cd..6ece47fd5ba6058fb24c89d0e891a1d18455c124 100644 (file)
@@ -40,12 +40,10 @@ class ActivityLogger
      */
     protected function newActivityForUser(string $type): Activity
     {
-        $ip = request()->ip() ?? '';
-
         return (new Activity())->forceFill([
             'type'     => strtolower($type),
             'user_id'  => user()->id,
-            'ip'       => config('app.env') === 'demo' ? '127.0.0.1' : $ip,
+            'ip'       => IpFormatter::fromCurrentRequest()->format(),
         ]);
     }
 
diff --git a/app/Actions/IpFormatter.php b/app/Actions/IpFormatter.php
new file mode 100644 (file)
index 0000000..3ca4b6e
--- /dev/null
@@ -0,0 +1,81 @@
+<?php
+
+namespace BookStack\Actions;
+
+class IpFormatter
+{
+    protected string $ip;
+    protected int $precision;
+
+    public function __construct(string $ip, int $precision)
+    {
+        $this->ip = trim($ip);
+        $this->precision = max(0, min($precision, 4));
+    }
+
+    public function format(): string
+    {
+        if (empty($this->ip) || $this->precision === 4) {
+            return $this->ip;
+        }
+
+        return $this->isIpv6() ? $this->maskIpv6() : $this->maskIpv4();
+    }
+
+    protected function maskIpv4(): string
+    {
+        $exploded = $this->explodeAndExpandIp('.', 4);
+        $maskGroupCount = min( 4 - $this->precision, count($exploded));
+
+        for ($i = 0; $i < $maskGroupCount; $i++) {
+            $exploded[3 - $i] = 'x';
+        }
+
+        return implode('.', $exploded);
+    }
+
+    protected function maskIpv6(): string
+    {
+        $exploded = $this->explodeAndExpandIp(':', 8);
+        $maskGroupCount = min(8 - ($this->precision * 2), count($exploded));
+
+        for ($i = 0; $i < $maskGroupCount; $i++) {
+            $exploded[7 - $i] = 'x';
+        }
+
+        return implode(':', $exploded);
+    }
+
+    protected function isIpv6(): bool
+    {
+        return strpos($this->ip, ':') !== false;
+    }
+
+    protected function explodeAndExpandIp(string $separator, int $targetLength): array
+    {
+        $exploded = explode($separator, $this->ip);
+
+        while (count($exploded) < $targetLength) {
+            $emptyIndex = array_search('', $exploded) ?: count($exploded) - 1;
+            array_splice($exploded, $emptyIndex, 0, '0');
+        }
+
+        $emptyIndex = array_search('', $exploded);
+        if ($emptyIndex !== false) {
+            $exploded[$emptyIndex] = '0';
+        }
+
+        return $exploded;
+    }
+
+    public static function fromCurrentRequest(): self
+    {
+        $ip = request()->ip() ?? '';
+
+        if (config('app.env') === 'demo') {
+            $ip = '127.0.0.1';
+        }
+
+        return new self($ip, config('app.ip_address_precision'));
+    }
+}
\ No newline at end of file
index a164de1fa46f6ad5e30582ae5884f348867c71d2..53d399abec46977f3a6f6917cc5514f8758859c3 100644 (file)
@@ -64,6 +64,10 @@ return [
     // Current host and source for the "DRAWIO" setting will be auto-appended to the sources configured.
     'iframe_sources' => env('ALLOWED_IFRAME_SOURCES', 'https://*.draw.io https://*.youtube.com https://*.youtube-nocookie.com https://*.vimeo.com'),
 
+    // Alter the precision of IP addresses stored by BookStack.
+    // Integer value between 0 (IP hidden) to 4 (Full IP usage)
+    'ip_address_precision' => env('IP_ADDRESS_PRECISION', 4),
+
     // Application timezone for back-end date functions.
     'timezone' => env('APP_TIMEZONE', 'UTC'),
 
index 56a510b101d14fb28a140128ef48ef0a99418732..cba6e40a983a53f2fd5edb72188bcfa1cabbc27c 100644 (file)
@@ -57,5 +57,6 @@
     <server name="LOG_FAILED_LOGIN_CHANNEL" value="testing"/>
     <server name="WKHTMLTOPDF" value="false"/>
     <server name="APP_DEFAULT_DARK_MODE" value="false"/>
+    <server name="IP_ADDRESS_PRECISION" value="4"/>
   </php>
 </phpunit>
index 8266fd972f1abc540c40baa45a7d4336b56c8c78..92289cd4ffe44e34e4f53b38d35d0a16971df1c6 100644 (file)
@@ -218,4 +218,27 @@ class AuditLogTest extends TestCase
             'entity_id' => $page->id,
         ]);
     }
+
+    public function test_ip_address_respects_precision_setting()
+    {
+        config()->set('app.proxies', '*');
+        config()->set('app.ip_address_precision', 2);
+        $editor = $this->getEditor();
+        /** @var Page $page */
+        $page = Page::query()->first();
+
+        $this->actingAs($editor)->put($page->getUrl(), [
+            'name' => 'Updated page',
+            'html' => '<p>Updated content</p>',
+        ], [
+            'X-Forwarded-For' => '192.123.45.1',
+        ])->assertRedirect($page->refresh()->getUrl());
+
+        $this->assertDatabaseHas('activities', [
+            'type'      => ActivityType::PAGE_UPDATE,
+            'ip'        => '192.123.x.x',
+            'user_id'   => $editor->id,
+            'entity_id' => $page->id,
+        ]);
+    }
 }
diff --git a/tests/Unit/IpFormatterTest.php b/tests/Unit/IpFormatterTest.php
new file mode 100644 (file)
index 0000000..928b1ab
--- /dev/null
@@ -0,0 +1,32 @@
+<?php
+
+namespace Tests\Unit;
+
+use BookStack\Actions\IpFormatter;
+use Tests\TestCase;
+
+class IpFormatterTest extends TestCase
+{
+    public function test_ips_formatted_as_expected()
+    {
+        $this->assertEquals('192.123.45.5', (new IpFormatter('192.123.45.5', 4))->format());
+        $this->assertEquals('192.123.45.x', (new IpFormatter('192.123.45.5', 3))->format());
+        $this->assertEquals('192.123.x.x', (new IpFormatter('192.123.45.5', 2))->format());
+        $this->assertEquals('192.x.x.x', (new IpFormatter('192.123.45.5', 1))->format());
+        $this->assertEquals('x.x.x.x', (new IpFormatter('192.123.45.5', 0))->format());
+
+        $ipv6 = '2001:db8:85a3:8d3:1319:8a2e:370:7348';
+        $this->assertEquals($ipv6, (new IpFormatter($ipv6, 4))->format());
+        $this->assertEquals('2001:db8:85a3:8d3:1319:8a2e:x:x', (new IpFormatter($ipv6, 3))->format());
+        $this->assertEquals('2001:db8:85a3:8d3:x:x:x:x', (new IpFormatter($ipv6, 2))->format());
+        $this->assertEquals('2001:db8:x:x:x:x:x:x', (new IpFormatter($ipv6, 1))->format());
+        $this->assertEquals('x:x:x:x:x:x:x:x', (new IpFormatter($ipv6, 0))->format());
+    }
+
+    public function test_shortened_ipv6_addresses_expands_as_expected()
+    {
+        $this->assertEquals('2001:0:0:0:0:0:x:x', (new IpFormatter('2001::370:7348', 3))->format());
+        $this->assertEquals('2001:0:0:0:0:85a3:x:x', (new IpFormatter('2001::85a3:370:7348', 3))->format());
+        $this->assertEquals('2001:0:x:x:x:x:x:x', (new IpFormatter('2001::', 1))->format());
+    }
+}