Skip to content

Commit f2d68d2

Browse files
author
Daniel Neto
committed
fix: Update video_password handling to support bcrypt hashes and improve security
https://github.com/WWBN/AVideo/security/advisories/GHSA-363v-5rh8-23wg#event-592859
1 parent 994cc2b commit f2d68d2

File tree

7 files changed

+79
-38
lines changed

7 files changed

+79
-38
lines changed

install/checkConfiguration.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
}
2525

2626

27-
$installationVersion = "28.0";
27+
$installationVersion = "29.0";
2828

2929
require_once '../objects/functionsSecurity.php';
3030

install/database.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ CREATE TABLE IF NOT EXISTS `videos` (
169169
`only_for_paid` TINYINT(1) NULL DEFAULT NULL,
170170
`serie_playlists_id` INT(11) NULL DEFAULT NULL,
171171
`sites_id` INT(11) NULL DEFAULT NULL,
172-
`video_password` VARCHAR(45) NULL DEFAULT NULL,
172+
`video_password` VARCHAR(255) NULL DEFAULT NULL,
173173
`encoderURL` VARCHAR(255) NULL DEFAULT NULL,
174174
`filepath` VARCHAR(255) NULL DEFAULT NULL,
175175
`filesize` BIGINT(19) UNSIGNED NULL DEFAULT 0,

objects/functions.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6264,8 +6264,13 @@ function outputAndContinueInBackground($msg = '')
62646264
function cleanUpRowFromDatabase(&$row)
62656265
{
62666266
if (is_array($row)) {
6267+
// Convert video_password to a presence flag before stripping it,
6268+
// so callers never receive the stored hash.
6269+
if (array_key_exists('video_password', $row)) {
6270+
$row['video_password'] = empty($row['video_password']) ? '' : '1';
6271+
}
62676272
foreach ($row as $key => $value) {
6268-
if (preg_match('/pass/i', $key)) {
6273+
if ($key !== 'video_password' && preg_match('/pass/i', $key)) {
62696274
unset($row[$key]);
62706275
}
62716276
}

objects/video.php

Lines changed: 40 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,9 @@ class Video extends ObjectYPT
111111
const ASPECT_RATIO_VERTICAL = '9:16';
112112
const ASPECT_RATIO_HORIZONTAL = '16:9';
113113

114+
/** Sentinel value: tells setVideo_password() to leave the stored password unchanged. */
115+
const PASSWORD_KEEP = '__keep__';
116+
114117

115118
const SORT_TYPE_CHANNELSUGGESTED = 'channelSuggested';
116119
const SORT_TYPE_SUGGESTED = 'suggested';
@@ -520,10 +523,44 @@ public function getVideo_password()
520523
return trim($this->video_password);
521524
}
522525

526+
/**
527+
* Returns a bcrypt hash of the given plaintext password.
528+
* Already-hashed values (bcrypt prefix '$2') are returned as-is.
529+
* Empty string is returned unchanged (means "no password").
530+
*/
531+
public static function hashVideoPassword(string $plaintext): string
532+
{
533+
if ($plaintext === '' || substr($plaintext, 0, 2) === '$2') {
534+
return $plaintext;
535+
}
536+
return password_hash($plaintext, PASSWORD_BCRYPT);
537+
}
538+
539+
/**
540+
* Verifies a plaintext password against the stored value.
541+
* Handles both bcrypt hashes (modern) and legacy plaintext (backward-compat).
542+
*/
543+
public static function verifyVideoPassword(string $entered, string $stored): bool
544+
{
545+
if ($stored === '') {
546+
return true;
547+
}
548+
if (substr($stored, 0, 2) === '$2') {
549+
return password_verify($entered, $stored);
550+
}
551+
// Legacy plaintext — direct comparison until owner re-saves
552+
return $entered === $stored;
553+
}
554+
523555
public function setVideo_password($video_password)
524556
{
557+
$video_password = trim($video_password);
558+
if ($video_password === self::PASSWORD_KEEP) {
559+
return;
560+
}
561+
$video_password = self::hashVideoPassword($video_password);
525562
AVideoPlugin::onVideoSetVideo_password($this->id, $this->video_password, $video_password);
526-
$this->video_password = trim($video_password);
563+
$this->video_password = $video_password;
527564
}
528565

529566
public function save($updateVideoGroups = false, $allowOfflineUser = false)
@@ -2289,17 +2326,7 @@ static function getInfo($row, $getStatistcs = false)
22892326
if (!empty($obj['userExternalOptions']) && is_string($obj['userExternalOptions'])) {
22902327
$obj['userExternalOptions'] = User::decodeExternalOption($obj['userExternalOptions']);
22912328
}
2292-
$obj = cleanUpRowFromDatabase($obj);
2293-
2294-
if (!self::canEdit($obj['id'])) {
2295-
if (!empty($rowOriginal['video_password'])) {
2296-
$obj['video_password'] = '1';
2297-
} else {
2298-
$obj['video_password'] = '0';
2299-
}
2300-
} else {
2301-
$obj['video_password'] = empty($rowOriginal['video_password']) ? '' : $rowOriginal['video_password'];
2302-
}
2329+
$obj = cleanUpRowFromDatabase($obj); // video_password is reduced to '' / '1' flag inside
23032330
if (self::forceAudio()) {
23042331
$obj['type'] = 'audio';
23052332
} else if (self::forceArticle()) {
@@ -2403,16 +2430,7 @@ static function getInfoPersonal($row)
24032430
return object_to_array($cache);
24042431
}
24052432

2406-
$row = cleanUpRowFromDatabase($row);
2407-
if (!self::canEdit($row['id'])) {
2408-
if (!empty($rowOriginal['video_password'])) {
2409-
$row['video_password'] = '1';
2410-
} else {
2411-
$row['video_password'] = '0';
2412-
}
2413-
} else {
2414-
$row['video_password'] = empty($rowOriginal['video_password']) ? '' : $rowOriginal['video_password'];
2415-
}
2433+
$row = cleanUpRowFromDatabase($row); // video_password is reduced to '' / '1' flag inside
24162434
TimeLogEnd($timeLogName, __LINE__, $TimeLogLimit);
24172435
$row['isFavorite'] = self::isFavorite($row['id']);
24182436
TimeLogEnd($timeLogName, __LINE__, $TimeLogLimit);

plugin/CustomizeUser/CustomizeUser.php

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -796,20 +796,21 @@ public function getModeYouTube($videos_id)
796796
public static function videoPasswordIsGood($videos_id)
797797
{
798798
$video = new Video("", "", $videos_id);
799-
$videoPassword = $video->getVideo_password();
800-
if (empty($videoPassword)) {
799+
$storedHash = $video->getVideo_password();
800+
if (empty($storedHash)) {
801801
return true;
802802
}
803-
//var_dump($_REQUEST['video_password'], $videoPassword);exit;
804-
if (empty($_SESSION['video_password'][$videos_id]) || $videoPassword !== $_SESSION['video_password'][$videos_id]) {
805-
if (!empty($_REQUEST['video_password']) && $_REQUEST['video_password'] == $videoPassword) {
806-
_session_start();
807-
$_SESSION['video_password'][$videos_id] = $_REQUEST['video_password'];
808-
return true;
809-
}
810-
return false;
803+
// Session token is derived from the stored hash so it auto-invalidates when the password changes
804+
$sessionToken = hash('sha256', $storedHash);
805+
if (!empty($_SESSION['video_password_token'][$videos_id]) && $_SESSION['video_password_token'][$videos_id] === $sessionToken) {
806+
return true;
811807
}
812-
return true;
808+
if (!empty($_REQUEST['video_password']) && Video::verifyVideoPassword($_REQUEST['video_password'], $storedHash)) {
809+
_session_start();
810+
$_SESSION['video_password_token'][$videos_id] = $sessionToken;
811+
return true;
812+
}
813+
return false;
813814
}
814815

815816
public function getEmbed($videos_id)

updatedb/updateDb.v29.0.sql

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0;
2+
SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0;
3+
SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='TRADITIONAL,ALLOW_INVALID_DATES';
4+
5+
-- Expand video_password to hold bcrypt hashes (60 chars); VARCHAR(255) future-proofs
6+
-- against longer algorithm outputs per PHP password_hash() documentation.
7+
ALTER TABLE `videos`
8+
MODIFY COLUMN `video_password` VARCHAR(255) NULL DEFAULT NULL;
9+
10+
UPDATE configurations SET version = '29.0', modified = now() WHERE id = 1;
11+
12+
SET SQL_MODE=@OLD_SQL_MODE;
13+
SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS;
14+
SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS;

view/managerVideos_body.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -685,6 +685,7 @@
685685
var isArticle = 0;
686686
var checkProgressTimeout = [];
687687
var filterDateRange = '';
688+
var VIDEO_PASSWORD_KEEP = <?php echo json_encode(Video::PASSWORD_KEEP); ?>;
688689

689690

690691
function saveVideoOnPlaylist(videos_id, add, playlists_id) {
@@ -944,7 +945,9 @@ function editVideo(row) {
944945

945946
$('#inputVideoId').val(row.id);
946947
$('#inputTitle').val(row.title);
947-
$('#inputVideoPassword').val(row.video_password);
948+
// video_password is a flag ('1'/''): never pre-populate the field with the stored hash
949+
$('#inputVideoPassword').val('').data('hasPassword', row.video_password === '1' || row.video_password === 1);
950+
// VIDEO_PASSWORD_KEEP is the PHP constant Video::PASSWORD_KEEP passed to JS below
948951
$('#videoStatus').val(row.status);
949952
$('#inputTrailer').val(row.trailer1);
950953
$('#inputCleanTitle').val(row.clean_title);
@@ -1190,7 +1193,7 @@ function saveVideo(closeModal) {
11901193
?> "id": $('#inputVideoId').val(),
11911194
"title": $('#inputTitle').val(),
11921195
"trailer1": $('#inputTrailer').val(),
1193-
"video_password": $('#inputVideoPassword').val(),
1196+
"video_password": ($('#inputVideoPassword').val() !== '') ? $('#inputVideoPassword').val() : ($('#inputVideoPassword').data('hasPassword') ? VIDEO_PASSWORD_KEEP : ''),
11941197
"videoStatus": $('#videoStatus').val(),
11951198
"videoLink": $('#videoLink').val(),
11961199
"epg_link": $('#epg_link').val(),

0 commit comments

Comments
 (0)