mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2026-05-04 12:32:36 +00:00
fix(web): escape HTML in commit messages to prevent XSS (#6523)
Signed-off-by: wucm667 <stevenwucongmin@gmail.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import { useI18n } from 'vue-i18n';
|
||||
import { useDate } from '~/compositions/useDate';
|
||||
import { useElapsedTime } from '~/compositions/useElapsedTime';
|
||||
import type { Pipeline } from '~/lib/api/types';
|
||||
import { escapeHtml } from '~/lib/utils';
|
||||
|
||||
const { toLocaleString, timeAgo, prettyDuration } = useDate();
|
||||
|
||||
@@ -75,10 +76,10 @@ export default (pipeline: Ref<Pipeline | undefined>) => {
|
||||
return prettyDuration(durationElapsed.value);
|
||||
});
|
||||
|
||||
const message = computed(() => emojify(pipeline.value?.message ?? ''));
|
||||
const message = computed(() => emojify(escapeHtml(pipeline.value?.message ?? '')));
|
||||
const shortMessage = computed(() => message.value.split('\n')[0]);
|
||||
|
||||
const prTitleWithDescription = computed(() => emojify(pipeline.value?.title ?? ''));
|
||||
const prTitleWithDescription = computed(() => emojify(escapeHtml(pipeline.value?.title ?? '')));
|
||||
const prTitle = computed(() => prTitleWithDescription.value.split('\n')[0]);
|
||||
|
||||
const prettyRef = computed(() => {
|
||||
|
||||
45
web/src/lib/utils.test.ts
Normal file
45
web/src/lib/utils.test.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { escapeHtml } from './utils';
|
||||
|
||||
describe('escapeHtml', () => {
|
||||
it('should return plain text unchanged', () => {
|
||||
expect(escapeHtml('hello world')).toBe('hello world');
|
||||
});
|
||||
|
||||
it('should return empty string unchanged', () => {
|
||||
expect(escapeHtml('')).toBe('');
|
||||
});
|
||||
|
||||
it('should escape HTML tags', () => {
|
||||
expect(escapeHtml('<b>bold</b>')).toBe('<b>bold</b>');
|
||||
expect(escapeHtml('<script>alert("xss")</script>')).toBe('<script>alert("xss")</script>');
|
||||
});
|
||||
|
||||
it('should escape ampersands', () => {
|
||||
expect(escapeHtml('foo & bar')).toBe('foo & bar');
|
||||
expect(escapeHtml('a&&b')).toBe('a&&b');
|
||||
});
|
||||
|
||||
it('should escape double quotes', () => {
|
||||
expect(escapeHtml('say "hello"')).toBe('say "hello"');
|
||||
});
|
||||
|
||||
it('should escape single quotes', () => {
|
||||
expect(escapeHtml("it's")).toBe('it's');
|
||||
});
|
||||
|
||||
it('should escape greater-than signs', () => {
|
||||
expect(escapeHtml('a > b')).toBe('a > b');
|
||||
});
|
||||
|
||||
it('should escape mixed content', () => {
|
||||
expect(escapeHtml(`<a href="foo">it's & that's <b>all</b>`)).toBe(
|
||||
'<a href="foo">it's & that's <b>all</b>',
|
||||
);
|
||||
});
|
||||
|
||||
it('should escape already-escaped ampersands', () => {
|
||||
expect(escapeHtml('&')).toBe('&amp;');
|
||||
});
|
||||
});
|
||||
@@ -11,3 +11,12 @@ export function debounce<T extends unknown[]>(fn: (...args: T) => void, delay: n
|
||||
export function deepClone<T>(value: T): T {
|
||||
return JSON.parse(JSON.stringify(toRaw(value))) as T;
|
||||
}
|
||||
|
||||
export function escapeHtml(text: string): string {
|
||||
return text
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user