Просмотр исходного кода

Merge branch 'master' into karthikkasturi-master

El RIDO 9 месяцев назад
Родитель
Сommit
2e5d6dddac

+ 7 - 0
.devcontainer/postCreateCommand.sh

@@ -1,9 +1,16 @@
 #!/bin/sh
 #!/bin/sh
 
 
 export PATH="$PATH:$HOME/.composer/vendor/bin"
 export PATH="$PATH:$HOME/.composer/vendor/bin"
+export PATH="$PATH:$PWD/vendor/bin"
+echo 'export PATH="$PATH:$HOME/.composer/vendor/bin"' >> ~/.bashrc
+echo 'export PATH="$PATH:$PWD/vendor/bin"' >> ~/.bashrc
 ln -s ./conf.sample.php cfg/conf.php
 ln -s ./conf.sample.php cfg/conf.php
 composer install --no-dev --optimize-autoloader
 composer install --no-dev --optimize-autoloader
 
 
+# for PHP unit testing
+# composer require google/cloud-storage
+# composer install --optimize-autoloader
+
 sudo chmod a+x "$(pwd)" && sudo rm -rf /var/www/html && sudo ln -s "$(pwd)" /var/www/html
 sudo chmod a+x "$(pwd)" && sudo rm -rf /var/www/html && sudo ln -s "$(pwd)" /var/www/html
 
 
 npm install --global nyc
 npm install --global nyc

+ 6 - 1
.github/workflows/snyk-scan.yml

@@ -12,7 +12,12 @@ jobs:
   # https://github.com/snyk/actions/tree/master/php
   # https://github.com/snyk/actions/tree/master/php
   snyk-php:
   snyk-php:
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
-    if: ${{ github.repository == 'PrivateBin/PrivateBin' }}
+    if: |
+      github.repository == 'PrivateBin/PrivateBin' && (
+      github.event.pull_request.author_association == 'COLLABORATOR' ||
+      github.event.pull_request.author_association == 'CONTRIBUTOR' ||
+      github.event.pull_request.author_association == 'MEMBER' ||
+      github.event.pull_request.author_association == 'OWNER' )
     steps:
     steps:
       - uses: actions/checkout@v5
       - uses: actions/checkout@v5
       - name: Install Google Cloud Storage
       - name: Install Google Cloud Storage

+ 2 - 0
.github/workflows/tests.yml

@@ -1,6 +1,8 @@
 name: Tests
 name: Tests
 on:
 on:
   push:
   push:
+  pull_request:
+    branches: [ master ]
   workflow_dispatch:
   workflow_dispatch:
 
 
 jobs:
 jobs:

+ 1 - 0
.gitignore

@@ -25,6 +25,7 @@ vendor/**/tst
 vendor/**/tests
 vendor/**/tests
 vendor/**/build_phar.php
 vendor/**/build_phar.php
 !vendor/**/*.php
 !vendor/**/*.php
+vendor/bin/**
 
 
 # Ignore local node modules, unit testing logs, api docs and IDE project files
 # Ignore local node modules, unit testing logs, api docs and IDE project files
 js/node_modules/
 js/node_modules/

+ 1 - 1
i18n/fr.json

@@ -224,7 +224,7 @@
     "Error decompressing document, your browser does not support WebAssembly. Please use another browser to view this document.": "Erreur lors de la décompression du document, votre navigateur ne supporte pas WebAssembly. Veuillez utiliser un autre navigateur pour voir ce document.",
     "Error decompressing document, your browser does not support WebAssembly. Please use another browser to view this document.": "Erreur lors de la décompression du document, votre navigateur ne supporte pas WebAssembly. Veuillez utiliser un autre navigateur pour voir ce document.",
     "Start over": "Recommencer",
     "Start over": "Recommencer",
     "Document copied to clipboard": "Document copié dans le presse-papier",
     "Document copied to clipboard": "Document copié dans le presse-papier",
-    "To copy document press on the copy button or use the clipboard shortcut <kbd>Ctrl</kbd>+<kbd>c</kbd>/<kbd>Cmd</kbd>+<kbd>c</kbd>": "Pour copier-coller appuyer sur le bouton To copy document press on the copy button or use the clipboard shortcut <kbd>Ctrl</kbd>+<kbd>c</kbd>/<kbd>Cmd</kbd>+<kbd>c</kbd>",
+    "To copy document press on the copy button or use the clipboard shortcut <kbd>Ctrl</kbd>+<kbd>c</kbd>/<kbd>Cmd</kbd>+<kbd>c</kbd>": "Pour copier appuyer sur le bouton Copier le lien ou utiliser le raccourci <kbd>Ctrl</kbd>+<kbd>c</kbd>/<kbd>Cmd</kbd>+<kbd>c</kbd>",
     "Copy link": "Copier le lien",
     "Copy link": "Copier le lien",
     "Link copied to clipboard": "Lien copié dans le presse-papier",
     "Link copied to clipboard": "Lien copié dans le presse-papier",
     "Document text": "Texte du document",
     "Document text": "Texte du document",

+ 2 - 2
js/legacy.js

@@ -106,8 +106,8 @@
                 return window.isSecureContext;
                 return window.isSecureContext;
             }
             }
 
 
-            // HTTP is obviously insecure
-            if (window.location.protocol !== 'http:') {
+            // HTTPS is considered secure
+            if (window.location.protocol === 'https:') {
                 return true;
                 return true;
             }
             }
 
 

+ 5 - 4
js/privatebin.js

@@ -930,7 +930,8 @@ jQuery.PrivateBin = (function($) {
             }
             }
 
 
             // load strings from JSON
             // load strings from JSON
-            $.getJSON('i18n/' + newLanguage + '.json', function(data) {
+            const cacheBreaker = document.querySelector('script[src^="js/privatebin.js"]').getAttribute('src').split('.js')[1] || '';
+            $.getJSON('i18n/' + newLanguage + '.json' + cacheBreaker, function(data) {
                 language = newLanguage;
                 language = newLanguage;
                 translations = data;
                 translations = data;
                 $(document).triggerHandler(languageLoadedEvent);
                 $(document).triggerHandler(languageLoadedEvent);
@@ -5160,7 +5161,7 @@ jQuery.PrivateBin = (function($) {
 
 
             // UI loading state
             // UI loading state
             TopNav.hideAllButtons();
             TopNav.hideAllButtons();
-            Alert.showLoading('Sending paste…', 'cloud-upload');
+            Alert.showLoading('Sending document…', 'cloud-upload');
             TopNav.collapseBar();
             TopNav.collapseBar();
 
 
             // get data
             // get data
@@ -5431,7 +5432,7 @@ jQuery.PrivateBin = (function($) {
         {
         {
             Alert.hideMessages();
             Alert.hideMessages();
             Alert.setCustomHandler(null);
             Alert.setCustomHandler(null);
-            Alert.showLoading('Decrypting paste…', 'cloud-download');
+            Alert.showLoading('Decrypting document…', 'cloud-download');
 
 
             if (typeof paste === 'undefined' || paste.type === 'click') {
             if (typeof paste === 'undefined' || paste.type === 'click') {
                 // get cipher data and wait until it is available
                 // get cipher data and wait until it is available
@@ -5776,7 +5777,7 @@ jQuery.PrivateBin = (function($) {
             // Important: This *must not* run Alert.hideMessages() as previous
             // Important: This *must not* run Alert.hideMessages() as previous
             // errors from viewing a document should be shown.
             // errors from viewing a document should be shown.
             TopNav.hideAllButtons();
             TopNav.hideAllButtons();
-            Alert.showLoading('Preparing new paste…', 'time');
+            Alert.showLoading('Preparing new document…', 'time');
 
 
             PasteStatus.hideMessages();
             PasteStatus.hideMessages();
             PasteViewer.hide();
             PasteViewer.hide();

+ 2 - 2
lib/Configuration.php

@@ -119,9 +119,9 @@ class Configuration
             'js/dark-mode-switch.js' => 'sha512-BhY7dNU14aDN5L+muoUmA66x0CkYUWkQT0nxhKBLP/o2d7jE025+dvWJa4OiYffBGEFgmhrD/Sp+QMkxGMTz2g==',
             'js/dark-mode-switch.js' => 'sha512-BhY7dNU14aDN5L+muoUmA66x0CkYUWkQT0nxhKBLP/o2d7jE025+dvWJa4OiYffBGEFgmhrD/Sp+QMkxGMTz2g==',
             'js/jquery-3.7.1.js'     => 'sha512-v2CJ7UaYy4JwqLDIrZUI/4hqeoQieOmAZNXBeQyjo21dadnwR+8ZaIJVT8EE2iyI61OV8e6M8PP2/4hpQINQ/g==',
             'js/jquery-3.7.1.js'     => 'sha512-v2CJ7UaYy4JwqLDIrZUI/4hqeoQieOmAZNXBeQyjo21dadnwR+8ZaIJVT8EE2iyI61OV8e6M8PP2/4hpQINQ/g==',
             'js/kjua-0.10.0.js'      => 'sha512-BYj4xggowR7QD150VLSTRlzH62YPfhpIM+b/1EUEr7RQpdWAGKulxWnOvjFx1FUlba4m6ihpNYuQab51H6XlYg==',
             'js/kjua-0.10.0.js'      => 'sha512-BYj4xggowR7QD150VLSTRlzH62YPfhpIM+b/1EUEr7RQpdWAGKulxWnOvjFx1FUlba4m6ihpNYuQab51H6XlYg==',
-            'js/legacy.js'           => 'sha512-08+subq1Lo+r+la5ENqeXiMgNJcVaaTtBIFGkrjziSpvtgCId3Jtin4/OkSdHYSoeztwwIab8uvCzPKHta6puQ==',
+            'js/legacy.js'           => 'sha512-mXAgFn/DonfPANvPO6Kf08zRKCeQ75jXK69gcUUOpPLFkp3KHnDhnvij8nEJxlutD/670Bfi4RNMG6uEjA4nNQ==',
             'js/prettify.js'         => 'sha512-puO0Ogy++IoA2Pb9IjSxV1n4+kQkKXYAEUtVzfZpQepyDPyXk8hokiYDS7ybMogYlyyEIwMLpZqVhCkARQWLMg==',
             'js/prettify.js'         => 'sha512-puO0Ogy++IoA2Pb9IjSxV1n4+kQkKXYAEUtVzfZpQepyDPyXk8hokiYDS7ybMogYlyyEIwMLpZqVhCkARQWLMg==',
-            'js/privatebin.js'       => 'sha512-seF8mXWh9E9SBK0qWNfm8RTGneGc05QpDcPyeTk+sg6efB6Edhiu6mvbSDCM4zb/gFqeko7jTYcUSYxhs3UIaA==',
+            'js/privatebin.js'       => 'sha512-nMz7sgy/jMKXKAUDcZcsUdVEuKN89Fgf0mvD7DB70pI4R6ExJwCOWaiQTc/czhVVUEwKzbhWrJG/hX2Tqhe5QA==',
             'js/purify-3.2.6.js'     => 'sha512-zqwL4OoBLFx89QPewkz4Lz5CSA2ktU+f31fuECkF0iK3Id5qd3Zpq5dMby8KwHjIEpsUgOqwF58cnmcaNem0EA==',
             'js/purify-3.2.6.js'     => 'sha512-zqwL4OoBLFx89QPewkz4Lz5CSA2ktU+f31fuECkF0iK3Id5qd3Zpq5dMby8KwHjIEpsUgOqwF58cnmcaNem0EA==',
             'js/showdown-2.1.0.js'   => 'sha512-WYXZgkTR0u/Y9SVIA4nTTOih0kXMEd8RRV6MLFdL6YU8ymhR528NLlYQt1nlJQbYz4EW+ZsS0fx1awhiQJme1Q==',
             'js/showdown-2.1.0.js'   => 'sha512-WYXZgkTR0u/Y9SVIA4nTTOih0kXMEd8RRV6MLFdL6YU8ymhR528NLlYQt1nlJQbYz4EW+ZsS0fx1awhiQJme1Q==',
             'js/zlib-1.3.1-1.js'     => 'sha512-5bU9IIP4PgBrOKLZvGWJD4kgfQrkTz8Z3Iqeu058mbQzW3mCumOU6M3UVbVZU9rrVoVwaW4cZK8U8h5xjF88eQ==',
             'js/zlib-1.3.1-1.js'     => 'sha512-5bU9IIP4PgBrOKLZvGWJD4kgfQrkTz8Z3Iqeu058mbQzW3mCumOU6M3UVbVZU9rrVoVwaW4cZK8U8h5xjF88eQ==',

+ 8 - 1
lib/Proxy/AbstractProxy.php

@@ -49,7 +49,14 @@ abstract class AbstractProxy
      */
      */
     public function __construct(Configuration $conf, string $link)
     public function __construct(Configuration $conf, string $link)
     {
     {
-        if (!str_starts_with($link, $conf->getKey('basepath') . '?')) {
+        if (!filter_var($link, FILTER_VALIDATE_URL, FILTER_FLAG_PATH_REQUIRED | FILTER_FLAG_QUERY_REQUIRED)) {
+            $this->_error = 'Invalid URL given.';
+            return;
+        }
+
+        if (!str_starts_with($link, $conf->getKey('basepath') . '?') ||
+            parse_url($link, PHP_URL_HOST) != parse_url($conf->getKey('basepath'), PHP_URL_HOST)
+        ) {
             $this->_error = 'Trying to shorten a URL that isn\'t pointing at our instance.';
             $this->_error = 'Trying to shorten a URL that isn\'t pointing at our instance.';
             return;
             return;
         }
         }

+ 58 - 4
tst/YourlsProxyTest.php

@@ -45,22 +45,76 @@ class YourlsProxyTest extends TestCase
         $yourls = new YourlsProxy($this->_conf, 'https://example.com/?foo#bar');
         $yourls = new YourlsProxy($this->_conf, 'https://example.com/?foo#bar');
         $this->assertFalse($yourls->isError());
         $this->assertFalse($yourls->isError());
         $this->assertEquals($yourls->getUrl(), 'https://example.com/1');
         $this->assertEquals($yourls->getUrl(), 'https://example.com/1');
+
+        $yourls = new YourlsProxy($this->_conf, 'https://example.com/?@foreign.malicious.example?foo#bar');
+        $this->assertFalse($yourls->isError());
+        $this->assertEquals($yourls->getUrl(), 'https://example.com/1');
+    }
+
+    /**
+     * @dataProvider providerInvalidUrl
+     */
+    public function testImvalidUrl($url): void
+    {
+        $yourls = new YourlsProxy($this->_conf, $url);
+        $this->assertTrue($yourls->isError());
+        $this->assertEquals($yourls->getError(), 'Invalid URL given.');
+    }
+
+    public function providerInvalidUrl(): array
+    {
+        return array(
+            array(''),
+            array(' '),
+            array('foo'),
+            array('https://'),
+            array('https://example.com'), // missing path and query parameter,
+            array('https://example.com/'), // missing query parameter
+            array('https://example.com?paste=something'), // missing path parameter
+            array('https://example.com@foreign.malicious.example?foo#bar'), // missing path parameter
+        );
     }
     }
 
 
-    public function testForeignUrl()
+    /**
+     * This tests for a trick using username of an URI, see:
+     * {@see https://cloud.google.com/blog/topics/threat-intelligence/url-obfuscation-schema-abuse/?hl=en}
+     *
+     * @dataProvider providerForeignUrlUsernameTrick
+     */
+    public function testForeignUrlUsingUsernameTrick($url): void
     {
     {
-        $yourls = new YourlsProxy($this->_conf, 'https://other.example.com/?foo#bar');
+        $yourls = new YourlsProxy($this->_conf, $url);
         $this->assertTrue($yourls->isError());
         $this->assertTrue($yourls->isError());
         $this->assertEquals($yourls->getError(), 'Trying to shorten a URL that isn\'t pointing at our instance.');
         $this->assertEquals($yourls->getError(), 'Trying to shorten a URL that isn\'t pointing at our instance.');
     }
     }
 
 
-    public function testSneakyForeignUrl()
+    public function providerForeignUrlUsernameTrick(): array
+    {
+        return array(
+            array('https://example.com@foreign.malicious.example/?foo#bar'),
+            array('https://example.com/@foreign.malicious.example?foo#bar'),
+        );
+    }
+
+    /**
+     * @dataProvider providerForeignUrl
+     */
+    public function testForeignUrl($url): void
     {
     {
-        $yourls = new YourlsProxy($this->_conf, 'https://other.example.com/?q=https://example.com/?foo#bar');
+        $yourls = new YourlsProxy($this->_conf, $url);
         $this->assertTrue($yourls->isError());
         $this->assertTrue($yourls->isError());
         $this->assertEquals($yourls->getError(), 'Trying to shorten a URL that isn\'t pointing at our instance.');
         $this->assertEquals($yourls->getError(), 'Trying to shorten a URL that isn\'t pointing at our instance.');
     }
     }
 
 
+    public function providerForeignUrl(): array
+    {
+        return array(
+            array('ftp://example.com/?n=np'), // wrong protocol
+            array('https://other.example.com/?foo#bar'), // wrong domain
+            array('https://other.example.com/?q=https://example.com/?foo#bar'), // domain included inside string
+        );
+    }
+
     public function testYourlsError()
     public function testYourlsError()
     {
     {
         // when statusCode is not 200, shorturl may not have been set
         // when statusCode is not 200, shorturl may not have been set