fix(web): escape HTML in commit messages to prevent XSS (#6523)

Signed-off-by: wucm667 <stevenwucongmin@gmail.com>
This commit is contained in:
wucm667
2026-05-01 09:07:20 +08:00
committed by GitHub
parent 9c0b2e7a4b
commit 1ffa588f87
3 changed files with 57 additions and 2 deletions

View File

@@ -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
View 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('&lt;b&gt;bold&lt;/b&gt;');
expect(escapeHtml('<script>alert("xss")</script>')).toBe('&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;');
});
it('should escape ampersands', () => {
expect(escapeHtml('foo & bar')).toBe('foo &amp; bar');
expect(escapeHtml('a&&b')).toBe('a&amp;&amp;b');
});
it('should escape double quotes', () => {
expect(escapeHtml('say "hello"')).toBe('say &quot;hello&quot;');
});
it('should escape single quotes', () => {
expect(escapeHtml("it's")).toBe('it&#x27;s');
});
it('should escape greater-than signs', () => {
expect(escapeHtml('a > b')).toBe('a &gt; b');
});
it('should escape mixed content', () => {
expect(escapeHtml(`<a href="foo">it's & that's <b>all</b>`)).toBe(
'&lt;a href=&quot;foo&quot;&gt;it&#x27;s &amp; that&#x27;s &lt;b&gt;all&lt;/b&gt;',
);
});
it('should escape already-escaped ampersands', () => {
expect(escapeHtml('&amp;')).toBe('&amp;amp;');
});
});

View File

@@ -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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#x27;');
}