Merge branch 'release/v8.1.x' into develop

This commit is contained in:
Chocobozzz
2026-05-23 18:26:05 +02:00
11 changed files with 194 additions and 54 deletions
+39
View File
@@ -19,6 +19,45 @@
* Fix column varchar lengths
## v8.1.8
### IMPORTANT NOTES
We have learned that the SQL injection vulnerability fixed in v8.1.6 has been exploited at scale since at least May 18, 2026 and so before the v8.1.6 release.
According to our investigation, the attacker exploited this SQL injection to generate a token for the `root` user and install the `peertube-plugin-google-analytics-js` plugin. This plugin imports a client script from `hxxps://www.googie-anaiytics.com/jquery.ui.js` that currently only logs a line in the web browser.
Actions taken by this release:
* Automatically remove `peertube-plugin-google-analytics-js` in v8.1.8
* Invalidate OAuth tokens in v8.1.8 (all users must log in again)
* Add a new `user.disable_root_auth` config key to disable `root` token usage
* Remove the plugin from the plugin registry
Actions taken by Framasoft:
* Report `googie-anaiytics.com` to the registrar
* Send a contact-form message to public PeerTube instances
* Release additional versions if we observe other attack vectors
* A CVE is being requested for the SQL injection
Actions admins must take:
* Upgrade to v8.1.8 **as soon as possible**
* Review newly created users and videos
* Review your instance configuration, especially *Configuration* -> *Customization* -> *JavaScript*/*CSS*
* Review installed plugins
* Generate new tokens for your runners
If you cannot upgrade to v8.1.8:
1. Remove actor follows that contain the `20.240.202.159` URL:
* Find them: `SELECT * FROM "actorFollow" WHERE "url" LIKE '%20.240.202.159%'`
* Delete them: `DELETE FROM "actorFollow" WHERE "id" = ...`
2. Remove actors that contain a `'` character in `inboxUrl`:
* Find them: `SELECT * FROM "actor" WHERE "inboxUrl" LIKE '%''%'`
* Delete them: `DELETE FROM "actor" WHERE "id" = ...`
3. Invalidate OAuth tokens: `UPDATE "oAuthToken" SET "accessTokenExpiresAt" = NOW(), "refreshTokenExpiresAt" = NOW() WHERE "accessTokenExpiresAt" > NOW() OR "refreshTokenExpiresAt" > NOW()`
4. Remove `peertube-plugin-google-analytics-js` from instance plugins
5. Disable federation in `production.yaml` by setting `federation.enabled` to `false`
6. Restart PeerTube
## v8.1.7
## Bug fixes
+4
View File
@@ -603,6 +603,10 @@ user:
# Enable or disable video history by default for new users
enabled: true
# Disable local login and OAuth token usage for the built-in `root` user.
# If enabled, root cannot login and existing root OAuth access/refresh tokens are rejected.
disable_root_auth: false
# Default value of maximum video bytes the user can upload
# Does not take into account transcoded files or account export archives (that can include user uploaded files)
# Byte format is supported ("1GB" etc)
+4
View File
@@ -613,6 +613,10 @@ user:
# Enable or disable video history by default for new users
enabled: true
# Disable local login and OAuth token usage for the built-in `root` user.
# If enabled, root cannot login and existing root OAuth access/refresh tokens are rejected.
disable_root_auth: false
# Default value of maximum video bytes the user can upload
# Does not take into account transcoded files or account export archives (that can include user uploaded files)
# Byte format is supported ("1GB" etc)
+35
View File
@@ -404,6 +404,41 @@ describe('Test oauth', function () {
})
})
describe('Disable root auth', function () {
let rootAccessToken: string
let rootRefreshToken: string
it('Should get root tokens when root auth is enabled', async function () {
await server.kill()
await server.run({ user: { disable_root_auth: false } })
const res = await server.login.login({ expectedStatus: HttpStatusCode.OK_200 })
rootAccessToken = res.access_token
rootRefreshToken = res.refresh_token
await server.users.getMyInfo({ token: rootAccessToken, expectedStatus: HttpStatusCode.OK_200 })
})
it('Should not allow root password login when root auth is disabled', async function () {
await server.kill()
await server.run({ user: { disable_root_auth: true } })
const body = await server.login.login({ expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
expect(body.code).to.equal(OAuth2ErrorCode.INVALID_GRANT)
})
it('Should reject existing root access token when root auth is disabled', async function () {
await server.users.getMyInfo({ token: rootAccessToken, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
})
it('Should reject existing root refresh token when root auth is disabled', async function () {
const { body } = await server.login.refreshToken({ refreshToken: rootRefreshToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
expect(body.code).to.equal(OAuth2ErrorCode.INVALID_GRANT)
})
})
describe('Custom token lifetime', function () {
before(async function () {
this.timeout(120_000)
@@ -65,6 +65,7 @@ export function checkMissedConfig () {
'open_telemetry.tracing.jaeger_exporter.endpoint',
'open_telemetry.metrics.http_request_duration.enabled',
'user.history.videos.enabled',
'user.disable_root_auth',
'user.video_quota',
'user.video_quota_daily',
'user.password_constraints.min_length',
+3
View File
@@ -607,6 +607,9 @@ const CONFIG = {
}
}
},
get DISABLE_ROOT_AUTH () {
return config.get<boolean>('user.disable_root_auth')
},
get VIDEO_QUOTA () {
return parseBytes(config.get<number>('user.video_quota'))
},
+1 -1
View File
@@ -62,7 +62,7 @@ import { CONFIG, registerConfigChangedHandler } from './config.js'
// ---------------------------------------------------------------------------
export const LAST_MIGRATION_VERSION = 1035
export const LAST_MIGRATION_VERSION = 1040
// ---------------------------------------------------------------------------
@@ -0,0 +1,29 @@
import * as Sequelize from 'sequelize'
async function up (utils: {
transaction: Sequelize.Transaction
queryInterface: Sequelize.QueryInterface
sequelize: Sequelize.Sequelize
}): Promise<void> {
const { transaction } = utils
await utils.sequelize.query(
`
UPDATE "oAuthToken"
SET
"accessTokenExpiresAt" = NOW(),
"refreshTokenExpiresAt" = NOW()
WHERE "accessTokenExpiresAt" > NOW() OR "refreshTokenExpiresAt" > NOW()
`,
{ transaction }
)
}
function down (options) {
throw new Error('Not implemented.')
}
export {
down,
up
}
+10
View File
@@ -58,6 +58,8 @@ async function getAccessToken (bearerToken: string) {
if (!tokenModel) return undefined
if (isRootAuthDisabled(tokenModel.User)) return undefined
if (tokenModel.User.pluginAuth) {
const valid = await PluginManager.Instance.isTokenValid(tokenModel, 'access')
@@ -81,6 +83,8 @@ async function getRefreshToken (refreshToken: string) {
const tokenModel = tokenInfo.token
if (isRootAuthDisabled(tokenModel.User)) return undefined
if (tokenModel.User.pluginAuth) {
const valid = await PluginManager.Instance.isTokenValid(tokenModel, 'refresh')
@@ -147,6 +151,8 @@ async function getUser (usernameOrEmail?: string, password?: string, options?: {
// If we don't find the user, or if the user belongs to a plugin
if (user?.pluginAuth !== null || !password) return null
if (isRootAuthDisabled(user)) return null
const passwordMatch = await user.isPasswordMatch(password)
if (passwordMatch !== true) return null
@@ -335,3 +341,7 @@ function checkUserValidityOrThrow (user: MUser, req: express.Request) {
function buildExpiresIn (expiresAt: Date) {
return Math.floor((expiresAt.getTime() - new Date().getTime()) / 1000)
}
function isRootAuthDisabled (user: Pick<MUser, 'username'>) {
return CONFIG.USER.DISABLE_ROOT_AUTH === true && user.username === 'root'
}
+20 -7
View File
@@ -1,9 +1,3 @@
import express from 'express'
import { createReadStream, createWriteStream } from 'fs'
import { ensureDir, outputFile, readJSON } from 'fs-extra/esm'
import { Server } from 'http'
import { createRequire } from 'module'
import { basename, join } from 'path'
import { getCompleteLocale, getHookType, internalRunHook } from '@peertube/peertube-core-utils'
import {
ClientScriptJSON,
@@ -19,6 +13,12 @@ import {
import { decachePlugin } from '@server/helpers/decache.js'
import { ApplicationModel } from '@server/models/application/application.js'
import { MOAuthTokenUser, MUser } from '@server/types/models/index.js'
import express from 'express'
import { createReadStream, createWriteStream } from 'fs'
import { ensureDir, outputFile, readJSON } from 'fs-extra/esm'
import { Server } from 'http'
import { createRequire } from 'module'
import { basename, join } from 'path'
import { isLibraryCodeValid, isPackageJSONValid } from '../../helpers/custom-validators/plugins.js'
import { logger } from '../../helpers/logger.js'
import { CONFIG } from '../../initializers/config.js'
@@ -31,8 +31,8 @@ import {
RegisterServerOptions
} from '../../types/plugins/index.js'
import { ClientHtml } from '../html/client-html.js'
import { RegisterHelpers } from './register-helpers.js'
import { installNpmPlugin, installNpmPluginFromDisk, rebuildNativePlugins, removeNpmPlugin } from './package-manager.js'
import { RegisterHelpers } from './register-helpers.js'
const require = createRequire(import.meta.url)
@@ -68,6 +68,10 @@ type PluginLocalesTranslations = {
[locale: string]: PluginTranslation
}
const UNSECURE_PLUGINS_TO_REMOVE = [
'peertube-plugin-google-analytics-js'
]
export class PluginManager implements ServerHook {
private static instance: PluginManager
@@ -300,6 +304,15 @@ export class PluginManager implements ServerHook {
this.sortHooksByPriority()
}
async removeUnsecurePluginsIfNeededBeforeRegistration () {
for (const npmName of UNSECURE_PLUGINS_TO_REMOVE) {
const plugin = await PluginModel.loadByNpmName(npmName)
if (!plugin || plugin.uninstalled === true) continue
await this.uninstall({ npmName, unregister: false })
}
}
// Don't need the plugin type since themes cannot register server code
async unregister (npmName: string) {
logger.info('Unregister plugin %s.', npmName)
+48 -46
View File
@@ -4,13 +4,13 @@ await registerOpentelemetryTracing()
process.title = 'peertube'
// ----------- Core checker -----------
import { checkMissedConfig, checkFFmpeg, checkNodeVersion } from './core/initializers/checker-before-init.js'
import { checkFFmpeg, checkMissedConfig, checkNodeVersion } from './core/initializers/checker-before-init.js'
// Do not use barrels because we don't want to load all modules here (we need to initialize database first)
import { initI18n, useI18n } from '@server/helpers/i18n.js'
import { logger } from './core/helpers/logger.js'
import { CONFIG } from './core/initializers/config.js'
import { API_VERSION, WEBSERVER, loadLanguages } from './core/initializers/constants.js'
import { logger } from './core/helpers/logger.js'
import { initI18n, useI18n } from '@server/helpers/i18n.js'
const missed = checkMissedConfig()
if (missed.length !== 0) {
@@ -31,7 +31,7 @@ try {
process.exit(-1)
}
import { checkConfig, checkActivityPubUrls, checkFFmpegVersion } from './core/initializers/checker-after-init.js'
import { checkActivityPubUrls, checkConfig, checkFFmpegVersion } from './core/initializers/checker-after-init.js'
try {
checkConfig()
@@ -43,7 +43,7 @@ try {
// ----------- Database -----------
// Initialize database and models
import { initDatabaseModels, checkDatabaseConnectionOrDie, sequelizeTypescript } from './core/initializers/database.js'
import { checkDatabaseConnectionOrDie, initDatabaseModels, sequelizeTypescript } from './core/initializers/database.js'
checkDatabaseConnectionOrDie()
import { migrate } from './core/initializers/migrator.js'
@@ -62,13 +62,13 @@ Promise.all([
]).catch(err => logger.error('Cannot load i18n/languages', { err }))
// Express configuration
import express from 'express'
import morgan, { token } from 'morgan'
import cors from 'cors'
import { program as cli } from 'commander'
import cookieParser from 'cookie-parser'
import cors from 'cors'
import express from 'express'
import { frameguard } from 'helmet'
import anonymize from 'ip-anonymize'
import { program as cli } from 'commander'
import morgan, { token } from 'morgan'
const app = express().disable('x-powered-by')
@@ -102,57 +102,57 @@ if (CONFIG.SECURITY.FRAMEGUARD.ENABLED) {
}
// ----------- PeerTube modules -----------
import { installApplication } from './core/initializers/installer.js'
import { Emailer } from './core/lib/emailer.js'
import { JobQueue } from './core/lib/job-queue/index.js'
import { HttpStatusCode } from '@peertube/peertube-models'
import { isTestOrDevInstance } from '@peertube/peertube-node-utils'
import { OpenTelemetryMetrics } from '@server/lib/opentelemetry/metrics.js'
import { RemoveExpiredUserExportsScheduler } from '@server/lib/schedulers/remove-expired-user-exports-scheduler.js'
import { UpdateTokenSessionScheduler } from '@server/lib/schedulers/update-token-session-scheduler.js'
import { VideoChannelSyncLatestScheduler } from '@server/lib/schedulers/video-channel-sync-latest-scheduler.js'
import { ServerConfigManager } from '@server/lib/server-config-manager.js'
import { VideoStatsManager } from '@server/lib/stats/video-stats-manager.js'
import { ApplicationModel } from '@server/models/application/application.js'
import {
activityPubRouter,
apiRouter,
miscRouter,
clientsRouter,
createWebsocketTrackerServer,
downloadRouter,
feedsRouter,
staticRouter,
wellKnownRouter,
lazyStaticRouter,
servicesRouter,
miscRouter,
objectStorageProxyRouter,
pluginsRouter,
trackerRouter,
createWebsocketTrackerServer,
servicesRouter,
sitemapRouter,
downloadRouter
staticRouter,
trackerRouter,
wellKnownRouter
} from './core/controllers/index.js'
import { advertiseDoNotTrack } from './core/middlewares/dnt.js'
import { apiFailMiddleware } from './core/middlewares/error.js'
import { Redis } from './core/lib/redis.js'
import { ActorFollowScheduler } from './core/lib/schedulers/actor-follow-scheduler.js'
import { RemoveOldStatsScheduler } from './core/lib/schedulers/remove-old-stats-scheduler.js'
import { UpdateVideosScheduler } from './core/lib/schedulers/update-videos-scheduler.js'
import { YoutubeDlUpdateScheduler } from './core/lib/schedulers/youtube-dl-update-scheduler.js'
import { VideosRedundancyScheduler } from './core/lib/schedulers/videos-redundancy-scheduler.js'
import { RemoveOldHistoryScheduler } from './core/lib/schedulers/remove-old-history-scheduler.js'
import { AutoFollowIndexInstances } from './core/lib/schedulers/auto-follow-index-instances.js'
import { RemoveDanglingResumableUploadsScheduler } from './core/lib/schedulers/remove-dangling-resumable-uploads-scheduler.js'
import { VideoStatsBufferScheduler } from './core/lib/schedulers/video-stats-buffer-scheduler.js'
import { GeoIPUpdateScheduler } from './core/lib/schedulers/geo-ip-update-scheduler.js'
import { RunnerJobWatchDogScheduler } from './core/lib/schedulers/runner-job-watch-dog-scheduler.js'
import { isHTTPSignatureDigestValid } from './core/helpers/peertube-crypto.js'
import { PeerTubeSocket } from './core/lib/peertube-socket.js'
import { installApplication } from './core/initializers/installer.js'
import { Emailer } from './core/lib/emailer.js'
import { updateStreamingPlaylistsInfohashesIfNeeded } from './core/lib/hls.js'
import { PluginsCheckScheduler } from './core/lib/schedulers/plugins-check-scheduler.js'
import { PeerTubeVersionCheckScheduler } from './core/lib/schedulers/peertube-version-check-scheduler.js'
import { JobQueue } from './core/lib/job-queue/index.js'
import { LiveManager } from './core/lib/live/index.js'
import { PeerTubeSocket } from './core/lib/peertube-socket.js'
import { Hooks } from './core/lib/plugins/hooks.js'
import { PluginManager } from './core/lib/plugins/plugin-manager.js'
import { LiveManager } from './core/lib/live/index.js'
import { HttpStatusCode } from '@peertube/peertube-models'
import { ServerConfigManager } from '@server/lib/server-config-manager.js'
import { VideoStatsManager } from '@server/lib/stats/video-stats-manager.js'
import { isTestOrDevInstance } from '@peertube/peertube-node-utils'
import { OpenTelemetryMetrics } from '@server/lib/opentelemetry/metrics.js'
import { ApplicationModel } from '@server/models/application/application.js'
import { VideoChannelSyncLatestScheduler } from '@server/lib/schedulers/video-channel-sync-latest-scheduler.js'
import { RemoveExpiredUserExportsScheduler } from '@server/lib/schedulers/remove-expired-user-exports-scheduler.js'
import { UpdateTokenSessionScheduler } from '@server/lib/schedulers/update-token-session-scheduler.js'
import { Redis } from './core/lib/redis.js'
import { ActorFollowScheduler } from './core/lib/schedulers/actor-follow-scheduler.js'
import { AutoFollowIndexInstances } from './core/lib/schedulers/auto-follow-index-instances.js'
import { GeoIPUpdateScheduler } from './core/lib/schedulers/geo-ip-update-scheduler.js'
import { PeerTubeVersionCheckScheduler } from './core/lib/schedulers/peertube-version-check-scheduler.js'
import { PluginsCheckScheduler } from './core/lib/schedulers/plugins-check-scheduler.js'
import { RemoveDanglingResumableUploadsScheduler } from './core/lib/schedulers/remove-dangling-resumable-uploads-scheduler.js'
import { RemoveOldHistoryScheduler } from './core/lib/schedulers/remove-old-history-scheduler.js'
import { RemoveOldStatsScheduler } from './core/lib/schedulers/remove-old-stats-scheduler.js'
import { RunnerJobWatchDogScheduler } from './core/lib/schedulers/runner-job-watch-dog-scheduler.js'
import { UpdateVideosScheduler } from './core/lib/schedulers/update-videos-scheduler.js'
import { VideoStatsBufferScheduler } from './core/lib/schedulers/video-stats-buffer-scheduler.js'
import { VideosRedundancyScheduler } from './core/lib/schedulers/videos-redundancy-scheduler.js'
import { YoutubeDlUpdateScheduler } from './core/lib/schedulers/youtube-dl-update-scheduler.js'
import { advertiseDoNotTrack } from './core/middlewares/dnt.js'
import { apiFailMiddleware } from './core/middlewares/error.js'
// ----------- Command line -----------
@@ -349,6 +349,8 @@ async function startApplication () {
server.listen(port, hostname, async () => {
if (cliOptions.plugins) {
try {
await PluginManager.Instance.removeUnsecurePluginsIfNeededBeforeRegistration()
await PluginManager.Instance.rebuildNativePluginsIfNeeded()
await PluginManager.Instance.registerPluginsAndThemes()