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 * 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 ## v8.1.7
## Bug fixes ## Bug fixes
+4
View File
@@ -603,6 +603,10 @@ user:
# Enable or disable video history by default for new users # Enable or disable video history by default for new users
enabled: true 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 # 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) # Does not take into account transcoded files or account export archives (that can include user uploaded files)
# Byte format is supported ("1GB" etc) # Byte format is supported ("1GB" etc)
+4
View File
@@ -613,6 +613,10 @@ user:
# Enable or disable video history by default for new users # Enable or disable video history by default for new users
enabled: true 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 # 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) # Does not take into account transcoded files or account export archives (that can include user uploaded files)
# Byte format is supported ("1GB" etc) # 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 () { describe('Custom token lifetime', function () {
before(async function () { before(async function () {
this.timeout(120_000) this.timeout(120_000)
@@ -65,6 +65,7 @@ export function checkMissedConfig () {
'open_telemetry.tracing.jaeger_exporter.endpoint', 'open_telemetry.tracing.jaeger_exporter.endpoint',
'open_telemetry.metrics.http_request_duration.enabled', 'open_telemetry.metrics.http_request_duration.enabled',
'user.history.videos.enabled', 'user.history.videos.enabled',
'user.disable_root_auth',
'user.video_quota', 'user.video_quota',
'user.video_quota_daily', 'user.video_quota_daily',
'user.password_constraints.min_length', '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 () { get VIDEO_QUOTA () {
return parseBytes(config.get<number>('user.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 (!tokenModel) return undefined
if (isRootAuthDisabled(tokenModel.User)) return undefined
if (tokenModel.User.pluginAuth) { if (tokenModel.User.pluginAuth) {
const valid = await PluginManager.Instance.isTokenValid(tokenModel, 'access') const valid = await PluginManager.Instance.isTokenValid(tokenModel, 'access')
@@ -81,6 +83,8 @@ async function getRefreshToken (refreshToken: string) {
const tokenModel = tokenInfo.token const tokenModel = tokenInfo.token
if (isRootAuthDisabled(tokenModel.User)) return undefined
if (tokenModel.User.pluginAuth) { if (tokenModel.User.pluginAuth) {
const valid = await PluginManager.Instance.isTokenValid(tokenModel, 'refresh') 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 we don't find the user, or if the user belongs to a plugin
if (user?.pluginAuth !== null || !password) return null if (user?.pluginAuth !== null || !password) return null
if (isRootAuthDisabled(user)) return null
const passwordMatch = await user.isPasswordMatch(password) const passwordMatch = await user.isPasswordMatch(password)
if (passwordMatch !== true) return null if (passwordMatch !== true) return null
@@ -335,3 +341,7 @@ function checkUserValidityOrThrow (user: MUser, req: express.Request) {
function buildExpiresIn (expiresAt: Date) { function buildExpiresIn (expiresAt: Date) {
return Math.floor((expiresAt.getTime() - new Date().getTime()) / 1000) 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 { getCompleteLocale, getHookType, internalRunHook } from '@peertube/peertube-core-utils'
import { import {
ClientScriptJSON, ClientScriptJSON,
@@ -19,6 +13,12 @@ import {
import { decachePlugin } from '@server/helpers/decache.js' import { decachePlugin } from '@server/helpers/decache.js'
import { ApplicationModel } from '@server/models/application/application.js' import { ApplicationModel } from '@server/models/application/application.js'
import { MOAuthTokenUser, MUser } from '@server/types/models/index.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 { isLibraryCodeValid, isPackageJSONValid } from '../../helpers/custom-validators/plugins.js'
import { logger } from '../../helpers/logger.js' import { logger } from '../../helpers/logger.js'
import { CONFIG } from '../../initializers/config.js' import { CONFIG } from '../../initializers/config.js'
@@ -31,8 +31,8 @@ import {
RegisterServerOptions RegisterServerOptions
} from '../../types/plugins/index.js' } from '../../types/plugins/index.js'
import { ClientHtml } from '../html/client-html.js' import { ClientHtml } from '../html/client-html.js'
import { RegisterHelpers } from './register-helpers.js'
import { installNpmPlugin, installNpmPluginFromDisk, rebuildNativePlugins, removeNpmPlugin } from './package-manager.js' import { installNpmPlugin, installNpmPluginFromDisk, rebuildNativePlugins, removeNpmPlugin } from './package-manager.js'
import { RegisterHelpers } from './register-helpers.js'
const require = createRequire(import.meta.url) const require = createRequire(import.meta.url)
@@ -68,6 +68,10 @@ type PluginLocalesTranslations = {
[locale: string]: PluginTranslation [locale: string]: PluginTranslation
} }
const UNSECURE_PLUGINS_TO_REMOVE = [
'peertube-plugin-google-analytics-js'
]
export class PluginManager implements ServerHook { export class PluginManager implements ServerHook {
private static instance: PluginManager private static instance: PluginManager
@@ -300,6 +304,15 @@ export class PluginManager implements ServerHook {
this.sortHooksByPriority() 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 // Don't need the plugin type since themes cannot register server code
async unregister (npmName: string) { async unregister (npmName: string) {
logger.info('Unregister plugin %s.', npmName) logger.info('Unregister plugin %s.', npmName)
+48 -46
View File
@@ -4,13 +4,13 @@ await registerOpentelemetryTracing()
process.title = 'peertube' process.title = 'peertube'
// ----------- Core checker ----------- // ----------- 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) // 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 { CONFIG } from './core/initializers/config.js'
import { API_VERSION, WEBSERVER, loadLanguages } from './core/initializers/constants.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() const missed = checkMissedConfig()
if (missed.length !== 0) { if (missed.length !== 0) {
@@ -31,7 +31,7 @@ try {
process.exit(-1) 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 { try {
checkConfig() checkConfig()
@@ -43,7 +43,7 @@ try {
// ----------- Database ----------- // ----------- Database -----------
// Initialize database and models // Initialize database and models
import { initDatabaseModels, checkDatabaseConnectionOrDie, sequelizeTypescript } from './core/initializers/database.js' import { checkDatabaseConnectionOrDie, initDatabaseModels, sequelizeTypescript } from './core/initializers/database.js'
checkDatabaseConnectionOrDie() checkDatabaseConnectionOrDie()
import { migrate } from './core/initializers/migrator.js' import { migrate } from './core/initializers/migrator.js'
@@ -62,13 +62,13 @@ Promise.all([
]).catch(err => logger.error('Cannot load i18n/languages', { err })) ]).catch(err => logger.error('Cannot load i18n/languages', { err }))
// Express configuration // Express configuration
import express from 'express' import { program as cli } from 'commander'
import morgan, { token } from 'morgan'
import cors from 'cors'
import cookieParser from 'cookie-parser' import cookieParser from 'cookie-parser'
import cors from 'cors'
import express from 'express'
import { frameguard } from 'helmet' import { frameguard } from 'helmet'
import anonymize from 'ip-anonymize' import anonymize from 'ip-anonymize'
import { program as cli } from 'commander' import morgan, { token } from 'morgan'
const app = express().disable('x-powered-by') const app = express().disable('x-powered-by')
@@ -102,57 +102,57 @@ if (CONFIG.SECURITY.FRAMEGUARD.ENABLED) {
} }
// ----------- PeerTube modules ----------- // ----------- PeerTube modules -----------
import { installApplication } from './core/initializers/installer.js' import { HttpStatusCode } from '@peertube/peertube-models'
import { Emailer } from './core/lib/emailer.js' import { isTestOrDevInstance } from '@peertube/peertube-node-utils'
import { JobQueue } from './core/lib/job-queue/index.js' 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 { import {
activityPubRouter, activityPubRouter,
apiRouter, apiRouter,
miscRouter,
clientsRouter, clientsRouter,
createWebsocketTrackerServer,
downloadRouter,
feedsRouter, feedsRouter,
staticRouter,
wellKnownRouter,
lazyStaticRouter, lazyStaticRouter,
servicesRouter, miscRouter,
objectStorageProxyRouter, objectStorageProxyRouter,
pluginsRouter, pluginsRouter,
trackerRouter, servicesRouter,
createWebsocketTrackerServer,
sitemapRouter, sitemapRouter,
downloadRouter staticRouter,
trackerRouter,
wellKnownRouter
} from './core/controllers/index.js' } 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 { 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 { updateStreamingPlaylistsInfohashesIfNeeded } from './core/lib/hls.js'
import { PluginsCheckScheduler } from './core/lib/schedulers/plugins-check-scheduler.js' import { JobQueue } from './core/lib/job-queue/index.js'
import { PeerTubeVersionCheckScheduler } from './core/lib/schedulers/peertube-version-check-scheduler.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 { Hooks } from './core/lib/plugins/hooks.js'
import { PluginManager } from './core/lib/plugins/plugin-manager.js' import { PluginManager } from './core/lib/plugins/plugin-manager.js'
import { LiveManager } from './core/lib/live/index.js' import { Redis } from './core/lib/redis.js'
import { HttpStatusCode } from '@peertube/peertube-models' import { ActorFollowScheduler } from './core/lib/schedulers/actor-follow-scheduler.js'
import { ServerConfigManager } from '@server/lib/server-config-manager.js' import { AutoFollowIndexInstances } from './core/lib/schedulers/auto-follow-index-instances.js'
import { VideoStatsManager } from '@server/lib/stats/video-stats-manager.js' import { GeoIPUpdateScheduler } from './core/lib/schedulers/geo-ip-update-scheduler.js'
import { isTestOrDevInstance } from '@peertube/peertube-node-utils' import { PeerTubeVersionCheckScheduler } from './core/lib/schedulers/peertube-version-check-scheduler.js'
import { OpenTelemetryMetrics } from '@server/lib/opentelemetry/metrics.js' import { PluginsCheckScheduler } from './core/lib/schedulers/plugins-check-scheduler.js'
import { ApplicationModel } from '@server/models/application/application.js' import { RemoveDanglingResumableUploadsScheduler } from './core/lib/schedulers/remove-dangling-resumable-uploads-scheduler.js'
import { VideoChannelSyncLatestScheduler } from '@server/lib/schedulers/video-channel-sync-latest-scheduler.js' import { RemoveOldHistoryScheduler } from './core/lib/schedulers/remove-old-history-scheduler.js'
import { RemoveExpiredUserExportsScheduler } from '@server/lib/schedulers/remove-expired-user-exports-scheduler.js' import { RemoveOldStatsScheduler } from './core/lib/schedulers/remove-old-stats-scheduler.js'
import { UpdateTokenSessionScheduler } from '@server/lib/schedulers/update-token-session-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 ----------- // ----------- Command line -----------
@@ -349,6 +349,8 @@ async function startApplication () {
server.listen(port, hostname, async () => { server.listen(port, hostname, async () => {
if (cliOptions.plugins) { if (cliOptions.plugins) {
try { try {
await PluginManager.Instance.removeUnsecurePluginsIfNeededBeforeRegistration()
await PluginManager.Instance.rebuildNativePluginsIfNeeded() await PluginManager.Instance.rebuildNativePluginsIfNeeded()
await PluginManager.Instance.registerPluginsAndThemes() await PluginManager.Instance.registerPluginsAndThemes()