Compare commits

..

No commits in common. "master" and "2.0.5" have entirely different histories.

16 changed files with 18 additions and 111 deletions

View file

@ -24,7 +24,7 @@ jobs:
persist-credentials: false
- name: Set up Node.js
uses: actions/setup-node@v7
uses: actions/setup-node@v6
with:
node-version: '20'

View file

@ -224,7 +224,7 @@ jobs:
persist-credentials: false
- name: Setup Node
uses: actions/setup-node@v7
uses: actions/setup-node@v6
with:
node-version: '20'
cache: 'npm'

View file

@ -1,10 +1,5 @@
# PrivateBin version history
## 2.0.6 (not yet released)
* CHANGED: Upgrading libraries to: DOMpurify 3.4.12
* FIXED: Gracefully handle YOURLS replies with a 200 status code but no shorturl, instead of raising a TypeError
* FIXED: Return "Invalid data." instead of HTTP 500 on malformed v2 JSON payloads (#1883)
## 2.0.5 (2026-07-11)
* CHANGED: Show OS-specific copy hotkey hint (Cmd+c on Mac, Ctrl+c on others) (#1506)
* FIXED: Prevent browsers from rendering unsafe attachments like HTML in a new tab

View file

@ -15,7 +15,7 @@ require('./prettify');
global.prettyPrint = window.PR.prettyPrint;
global.prettyPrintOne = window.PR.prettyPrintOne;
global.showdown = require('./showdown-2.1.0');
global.DOMPurify = require('./purify-3.4.12');
global.DOMPurify = require('./purify-3.4.1');
global.baseX = require('./base-x-5.0.1').baseX;
global.Legacy = require('./legacy').Legacy;
require('./privatebin');

11
js/package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "privatebin",
"version": "2.0.5",
"version": "2.0.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "privatebin",
"version": "2.0.5",
"version": "2.0.4",
"license": "zlib-acknowledgement",
"devDependencies": {
"@peculiar/webcrypto": "^1.5.0",
@ -3993,11 +3993,10 @@
"dev": true
},
"node_modules/ws": {
"version": "8.21.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
"integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
"version": "8.20.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz",
"integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},

3
js/purify-3.4.1.js Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -123,7 +123,7 @@ class Configuration
'js/legacy.js' => 'sha512-RQEo1hxpNc37i+jz/D9/JiAZhG8GFx3+SNxjYnI7jUgirDIqrCSj6QPAAZeaidditcWzsJ3jxfEj5lVm7ZwTRQ==',
'js/prettify.js' => 'sha512-puO0Ogy++IoA2Pb9IjSxV1n4+kQkKXYAEUtVzfZpQepyDPyXk8hokiYDS7ybMogYlyyEIwMLpZqVhCkARQWLMg==',
'js/privatebin.js' => 'sha512-g58rPgrKnvxBDnT6lzYazdEfgY3XhRh7Yt7i1jkJw4oSvN0k/VPZ4zBSL/dCGfXGIOHqlqmX8DO5UFv2XYqsXg==',
'js/purify-3.4.12.js' => 'sha512-Akf6HnAJZm0sWWWI4gp2GYff0NDnHUB02XJE5S7Hdq/Z5xtMjkuFacsDA8ZtViv1gi+onBxMhEMIaGyQeGxBng==',
'js/purify-3.4.1.js' => 'sha512-280a/Vb6fVFsYaeRrkuDp4EDmdYlt2XS+dlDEO/U9qljPrAraA2bIzHTNmP+9dpwPDDwTML+RS+h5iaagPwTzA==',
'js/showdown-2.1.0.js' => 'sha512-WYXZgkTR0u/Y9SVIA4nTTOih0kXMEd8RRV6MLFdL6YU8ymhR528NLlYQt1nlJQbYz4EW+ZsS0fx1awhiQJme1Q==',
'js/zlib-1.3.2.js' => 'sha512-RAhJgxg9siMIA8ky4c10Rc2zUgnK80olHB8Tt1IOYWY4Eh1WmrviQkDn+sgBlb38ZHq3tzufGC41kP360gmosQ==',
'js/zlib.js' => 'sha512-QOaEwssHqHRRcWJ2Un3Kl2Zhyprzl7T8zmsKN2FppFxW3VR+8UChYOx2iuL0HbXK42fuBWJm5PNQJxufulrt/w==',

View file

@ -56,21 +56,7 @@ class FormatV2
return false;
}
$cipherParams = $isComment ? $message['adata'] : ($message['adata'][0] ?? null);
// Make sure the cipher parameters are a properly sized array.
if (!is_array($cipherParams) || count($cipherParams) < 8) {
return false;
}
// Make sure the ciphertext and the cipher parameters used in the
// string operations below are actually strings, so that malformed
// input yields "Invalid data." instead of a fatal type error.
if (!is_string($message['ct']) ||
!is_string($cipherParams[0] ?? null) ||
!is_string($cipherParams[1] ?? null)) {
return false;
}
$cipherParams = $isComment ? $message['adata'] : $message['adata'][0];
// Make sure some fields are base64 data:
// - initialization vector
@ -133,7 +119,6 @@ class FormatV2
// require only the key 'expire' in the metadata of pastes
if (!$isComment && (
!is_array($message['meta']) ||
count($message['meta']) === 0 ||
!array_key_exists('expire', $message['meta']) ||
count($message['meta']) > 1

View file

@ -66,7 +66,7 @@ class YourlsProxy extends AbstractProxy
protected function _extractShortUrl(array $data): ?string
{
if ((int) ($data['statusCode'] ?? 0) === 200) {
return $data['shorturl'] ?? null;
return $data['shorturl'] ?? 0;
}
return null;
}

View file

@ -113,11 +113,6 @@ class Request
try {
$data = file_get_contents(self::$_inputStream);
$this->_params = Json::decode($data);
// a valid JSON scalar (number, bool or string) decodes
// without error, but is not a usable set of parameters
if (!is_array($this->_params)) {
$this->_params = array();
}
} catch (JsonException $e) {
// ignore error, $this->_params will remain empty
}
@ -194,6 +189,8 @@ class Request
foreach ($required_keys as $key) {
$data[$key] = $this->getParam($key, $key === 'v' ? 1 : '');
}
// forcing a cast to int or float
$data['v'] = $data['v'] + 0;
return $data;
}

View file

@ -66,7 +66,7 @@ if ($MARKDOWN) :
<?php
endif;
?>
<?php $this->_scriptTag('js/purify-3.4.12.js', 'defer'); ?>
<?php $this->_scriptTag('js/purify-3.4.1.js', 'defer'); ?>
<?php $this->_scriptTag('js/legacy.js', 'defer'); ?>
<?php $this->_scriptTag('js/privatebin.js', 'defer'); ?>
<!-- icon -->

View file

@ -50,7 +50,7 @@ if ($MARKDOWN) :
<?php
endif;
?>
<?php $this->_scriptTag('js/purify-3.4.12.js', 'defer'); ?>
<?php $this->_scriptTag('js/purify-3.4.1.js', 'defer'); ?>
<?php $this->_scriptTag('js/legacy.js', 'defer'); ?>
<?php $this->_scriptTag('js/privatebin.js', 'defer'); ?>
<!-- icon -->

View file

@ -72,41 +72,4 @@ class FormatV2Test extends TestCase
$paste = Helper::getPaste();
$this->assertFalse(FormatV2::isValid($paste), 'invalid meta key');
}
public function testFormatV2ValidatorRejectsMalformedTypes()
{
// malformed input must yield "false" (Invalid data) rather than a
// fatal type error under strict_types
$paste = Helper::getPastePost();
$paste['v'] = 'abc';
$this->assertFalse(FormatV2::isValid($paste), 'non-numeric version');
$paste = Helper::getPastePost();
$paste['v'] = array();
$this->assertFalse(FormatV2::isValid($paste), 'array version');
$paste = Helper::getPastePost();
$paste['ct'] = array('not', 'a', 'string');
$this->assertFalse(FormatV2::isValid($paste), 'non-string ciphertext');
$paste = Helper::getPastePost();
$paste['meta'] = 'not-an-array';
$this->assertFalse(FormatV2::isValid($paste), 'non-array meta');
$paste = Helper::getPastePost();
$paste['adata'][0] = 'not-an-array';
$this->assertFalse(FormatV2::isValid($paste), 'non-array cipher parameters');
$paste = Helper::getPastePost();
$paste['adata'][0] = array();
$this->assertFalse(FormatV2::isValid($paste), 'empty cipher parameters');
$paste = Helper::getPastePost();
$paste['adata'][0][0] = array();
$this->assertFalse(FormatV2::isValid($paste), 'non-string iv');
$comment = Helper::getCommentPost();
$comment['ct'] = 42;
$this->assertFalse(FormatV2::isValid($comment, true), 'non-string comment ciphertext');
}
}

View file

@ -158,26 +158,6 @@ class RequestTest extends TestCase
$this->assertEquals('create', $request->getOperation());
}
public function testPostScalarJson()
{
// a valid JSON scalar body (number, boolean or quoted string) must not
// be treated as a set of parameters, so that it is rejected cleanly
// instead of triggering a type error further down
foreach (array('1', '0.0', 'true', 'false', '"example"') as $scalar) {
$this->reset();
$_SERVER['REQUEST_METHOD'] = 'POST';
$_SERVER['HTTP_X_REQUESTED_WITH'] = 'JSONHttpRequest';
$file = Helper::createTempFile();
file_put_contents($file, $scalar);
Request::setInputStream($file);
$request = new Request;
unlink($file);
$this->assertTrue($request->isJsonApiCall(), 'is JSON API call');
$this->assertEquals('create', $request->getOperation());
$this->assertEquals('', $request->getParam('ct'), "scalar body {$scalar} yields no parameters");
}
}
public function testReadWithNegotiation()
{
$this->reset();

View file

@ -135,19 +135,6 @@ class YourlsProxyTest extends TestCase
$this->assertEquals($yourls->getError(), 'Proxy error: Error parsing proxy response. This can be a configuration issue, like wrong or missing config keys.');
}
public function testYourlsSuccessWithoutShortUrl()
{
// YOURLS may reply with statusCode 200 but without a shorturl field;
// this must be handled gracefully as an error instead of raising a
// TypeError (the method is declared to return ?string). YOURLS returns
// the status code as a string, so mirror that here.
file_put_contents($this->_mock_yourls_service, '{"statusCode":"200"}');
$yourls = new YourlsProxy($this->_conf, 'https://example.com/?foo#bar');
$this->assertTrue($yourls->isError());
$this->assertEquals($yourls->getError(), 'Proxy error: Error parsing proxy response. This can be a configuration issue, like wrong or missing config keys.');
}
public function testServerError()
{
// simulate some other server error that results in a non-JSON reply