mirror of
https://github.com/Chocobozzz/PeerTube.git
synced 2026-09-03 20:53:09 -05:00
Merge branch 'release/v8.1.x' into develop
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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'))
|
||||
},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user