Reverting usage of RE2 (WIP):

**Breaking changes**

The livechat v13 introduced a new library to handle regular expressions in forbidden words, to avoid
[ReDOS](https://en.wikipedia.org/wiki/ReDoS) attacks.
Unfortunately, this library was not able to install itself properly on some systems, and some admins were not able
to install the livechat plugin.

That's why we have disabled this library in v14, and introduce a new settings to enable regexp in forbidden words.
By default this settings is disabled, and your users won't be able to use regexp in their forbidden words.

The risk by enabling this feature is that a malicious user could cause a denial of service for the chat bot, by using a
special crafted regular expression in their channel options, and sending a special crafter message in one of their
rooms. If you trust your users (those who have rights to livestream), you can enable the settings. Otherwise it is not
recommanded. See the documentation for more informations.

**Minor changes and fixes**

* Channel's forbidden words: new "enable" column.
* New settings to enable regular expressions for channel forbidden words.
* "Channel advanced configuration" settings: removing the "experimental feature" label.
This commit is contained in:
John Livingston
2025-06-19 12:07:39 +02:00
parent e41529b61f
commit 3624dd5c3c
65 changed files with 343 additions and 2145 deletions

View File

@ -139,7 +139,7 @@ declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_ANONYMIZE_MODERATION_DESC: stri
declare const LOC_PROSODY_FIREWALL_CONFIGURATION: string
declare const LOC_PROSODY_FIREWALL_CONFIGURATION_HELP: string
declare const LOC_PROSODY_FIREWALL_DISABLED_WARNING: string
declare const LOC_PROSODY_FIREWALL_FILE_ENABLED: string
declare const LOC_ENABLED: string
declare const LOC_PROSODY_FIREWALL_NAME: string
declare const LOC_PROSODY_FIREWALL_NAME_DESC: string
declare const LOC_PROSODY_FIREWALL_CONTENT: string

View File

@ -15,7 +15,7 @@ import { html } from 'lit'
export function tplAdminFirewall (el: AdminFirewallElement): TemplateResult {
const tableHeaderList: DynamicFormHeader = {
enabled: {
colName: ptTr(LOC_PROSODY_FIREWALL_FILE_ENABLED)
colName: ptTr(LOC_ENABLED)
},
name: {
colName: ptTr(LOC_PROSODY_FIREWALL_NAME),

View File

@ -25,6 +25,9 @@ export class ChannelConfigurationElement extends LivechatElement {
@state()
public channelConfiguration?: ChannelConfiguration
@property({ attribute: false })
public enableUsersRegexp?: boolean
@provide({ context: channelDetailsServiceContext })
private _channelDetailsService?: ChannelDetailsService
@ -46,6 +49,7 @@ export class ChannelConfigurationElement extends LivechatElement {
task: async () => {
this._channelDetailsService = new ChannelDetailsService(this.ptOptions)
this.channelConfiguration = await this._channelDetailsService.fetchConfiguration(this.channelId ?? 0)
this.enableUsersRegexp = !!(await this.ptOptions.peertubeHelpers.getSettings())['enable-users-regexp']
this.actionDisabled = false // in case of reset
},
args: () => []
@ -85,8 +89,10 @@ export class ChannelConfigurationElement extends LivechatElement {
event?.preventDefault()
if (this._channelDetailsService && this.channelConfiguration) {
this.actionDisabled = true
this._channelDetailsService.saveOptions(this.channelConfiguration.channel.id,
this.channelConfiguration.configuration)
this._channelDetailsService.saveOptions(
this.channelConfiguration.channel.id,
this.channelConfiguration.configuration,
this.enableUsersRegexp ?? false)
.then(() => {
this.validationError = undefined
this.ptTranslate(LOC_SUCCESSFULLY_SAVED).then((msg) => {

View File

@ -15,13 +15,22 @@ import { noDuplicateMaxDelay, forbidSpecialCharsMaxTolerance } from 'shared/lib/
export function tplChannelConfiguration (el: ChannelConfigurationElement): TemplateResult {
const tableHeaderList: Record<string, DynamicFormHeader> = {
forbiddenWords: {
enabled: {
colName: ptTr(LOC_ENABLED)
},
entries: {
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_LABEL)
},
regexp: {
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REGEXP_LABEL),
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REGEXP_DESC)
},
...(
el.enableUsersRegexp
? {
regexp: {
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REGEXP_LABEL),
description: ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_FORBIDDEN_WORDS_REGEXP_DESC)
}
}
: {}
),
applyToModerators: {
colName: ptTr(LOC_LIVECHAT_CONFIGURATION_APPLYTOMODERATORS_LABEL),
description: ptTr(LOC_LIVECHAT_CONFIGURATION_APPLYTOMODERATORS_DESC)
@ -62,15 +71,25 @@ export function tplChannelConfiguration (el: ChannelConfigurationElement): Templ
}
const tableSchema: Record<string, DynamicFormSchema> = {
forbiddenWords: {
enabled: {
inputType: 'checkbox',
default: true
},
entries: {
inputType: 'tags',
default: [],
separator: '\n'
},
regexp: {
inputType: 'checkbox',
default: false
},
...(
el.enableUsersRegexp
? {
regexp: {
inputType: 'checkbox',
default: false
}
}
: {}
),
applyToModerators: {
inputType: 'checkbox',
default: false

View File

@ -151,9 +151,18 @@ export class ChannelDetailsService {
return true
}
frontToBack = (channelConfigurationOptions: ChannelConfigurationOptions): ChannelConfigurationOptions => {
// // This is a dirty hack, because backend wants seconds for botConf.quotes.delay, and front wants minutes.
frontToBack = (
channelConfigurationOptions: ChannelConfigurationOptions,
enableUsersRegexp: boolean
): ChannelConfigurationOptions => {
const c = JSON.parse(JSON.stringify(channelConfigurationOptions)) as ChannelConfigurationOptions // clone
if (!enableUsersRegexp) {
c.bot?.forbiddenWords.forEach(fw => {
fw.regexp = false // force to false
})
}
// This is a dirty hack, because backend wants seconds for botConf.quotes.delay, and front wants minutes.
c.bot?.quotes.forEach(q => {
if (typeof q.delay === 'number') {
q.delay = Math.round(q.delay * 60)
@ -175,7 +184,8 @@ export class ChannelDetailsService {
}
saveOptions = async (channelId: number,
channelConfigurationOptions: ChannelConfigurationOptions): Promise<Response> => {
channelConfigurationOptions: ChannelConfigurationOptions,
enableUsersRegexp: boolean): Promise<Response> => {
if (!await this.validateOptions(channelConfigurationOptions)) {
throw new Error('Invalid form data')
}
@ -186,7 +196,7 @@ export class ChannelDetailsService {
method: 'POST',
headers: this._headers,
body: JSON.stringify(
this.frontToBack(channelConfigurationOptions)
this.frontToBack(channelConfigurationOptions, enableUsersRegexp)
)
}
)