Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix timeline XSS #2230

Merged
merged 2 commits into from
Nov 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 103 additions & 3 deletions src/lichess/interfaces/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,14 +82,114 @@ export interface PongMessage {
readonly r: number
}

export type TimelineEntry =
BlogPostTimelineEntry
| FollowTimelineEntry
| ForumPostTimelineEntry
| GameEndTimelineEntry
| TourJoinTimelineEntry
| StudyCreateTimelineEntry
| StudyLikeTimelineEntry
| StreamStartTimelineEntry
| UblogPostTimelineEntry
| UblogLikeTimelineEntry

export type TimelineEntryType = 'follow' | 'game-end' | 'tour-join' | 'study-create' | 'study-like' | 'forum-post' | 'blog-post' | 'ublog-post' | 'ublog-post-like' | 'stream-start'

export interface TimelineEntry {
readonly data: any
interface BaseTimelineEntry {
readonly type: TimelineEntryType
readonly date: number
// added dynamically
fromNow: string
readonly type: TimelineEntryType
}

export interface BlogPostTimelineEntry extends BaseTimelineEntry {
readonly type: 'blog-post'
readonly data: {
readonly id: string
readonly slug: string
readonly title: string
}
}

export interface FollowTimelineEntry extends BaseTimelineEntry {
readonly type: 'follow'
readonly data: {
readonly u1: string
readonly u2: string
}
}

export interface ForumPostTimelineEntry extends BaseTimelineEntry {
readonly type: 'forum-post'
readonly data: {
readonly postId: string
readonly topicName: string
readonly userId: string
}
}

export interface GameEndTimelineEntry extends BaseTimelineEntry {
readonly type: 'game-end'
readonly data: {
readonly perf: PerfKey
readonly win?: string
readonly opponent: string
readonly playerId: string
}
}

export interface StreamStartTimelineEntry extends BaseTimelineEntry {
readonly type: 'stream-start'
readonly data: {
readonly date: number
readonly id: string
readonly name: string
}
}

type StudyTimelineEntryData = {
readonly studyId: string
readonly studyName: string
readonly userId: string
}

export interface StudyCreateTimelineEntry extends BaseTimelineEntry {
readonly type: 'study-create'
readonly data: StudyTimelineEntryData
}

export interface StudyLikeTimelineEntry extends BaseTimelineEntry {
readonly type: 'study-like'
readonly data: StudyTimelineEntryData
}

export interface TourJoinTimelineEntry extends BaseTimelineEntry {
readonly type: 'tour-join'
readonly data: {
readonly userId: string
readonly tourName: string
readonly tourId: string
}
}

export interface UblogPostTimelineEntry extends BaseTimelineEntry {
readonly type: 'ublog-post'
readonly data: {
readonly userId: string
readonly id: string
readonly slug: string
readonly title: string
}
}

export interface UblogLikeTimelineEntry extends BaseTimelineEntry {
readonly type: 'ublog-post-like'
readonly data: {
readonly id: string
readonly title: string
readonly userId: string
}
}

export interface TimelineData {
Expand Down
36 changes: 18 additions & 18 deletions src/ui/timeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { openWebsitePage } from '../utils/browse'
import { dropShadowHeader as headerWidget, backButton } from './shared/common'
import * as helper from './helper'
import layout from './layout'
import i18n, { fromNow } from '../i18n'
import { TimelineData, TimelineEntry, TimelineEntryType } from '../lichess/interfaces'
import i18n, { fromNow, i18nVdom } from '../i18n'
import { BlogPostTimelineEntry, FollowTimelineEntry, ForumPostTimelineEntry, GameEndTimelineEntry, StreamStartTimelineEntry, StudyCreateTimelineEntry, StudyLikeTimelineEntry, TimelineData, TimelineEntry, TimelineEntryType, TourJoinTimelineEntry, UblogLikeTimelineEntry, UblogPostTimelineEntry } from '../lichess/interfaces'
import { userTitle } from './user/userView'
import { LightUser } from '~/lichess/interfaces/user'

Expand Down Expand Up @@ -92,7 +92,7 @@ export function renderTimelineEntry(e: TimelineEntry, users: LightUserMap) {
}
}

function renderBlog(entry: TimelineEntry) {
function renderBlog(entry: BlogPostTimelineEntry) {
const data = entry.data
return h('li.list_item.timelineEntry.blogEntry', {
key: 'blog-post' + data.id,
Expand All @@ -105,49 +105,49 @@ function renderBlog(entry: TimelineEntry) {
])
}

function renderUblog(entry: TimelineEntry, users: LightUserMap) {
function renderUblog(entry: UblogPostTimelineEntry, users: LightUserMap) {
const data = entry.data
const actor = users[data.userId]
return h('li.list_item.timelineEntry', {
key: `ublog-post${data.id}`,
'data-external': `/@/${data.userId}/blog/${data.slug}/${data.id}`,
}, [
userTitle(false, actor.patron ?? false, actor.id, actor.title),
h.trust(i18n('xPublishedY', '', `<strong>${data.title}</strong>`)),
i18nVdom('xPublishedY', '', h('strong', data.title)),
' ',
h('small', h('em', entry.fromNow)),
])
}

function renderUblogLike(entry: TimelineEntry, users: LightUserMap) {
function renderUblogLike(entry: UblogLikeTimelineEntry, users: LightUserMap) {
const data = entry.data
const actor = users[data.userId]
return h('li.list_item.timelineEntry', {
key: `ublog-post-like${data.id}`,
'data-external': `/ublog/${data.id}/redirect`,
}, [
userTitle(false, actor.patron ?? false, actor.id, actor.title),
h.trust(i18n('xLikesY', '', `<strong>${data.title}</strong>`)),
i18nVdom('xLikesY', '', h('strong', data.title)),
' ',
h('small', h('em', entry.fromNow)),
])
}

function renderForum(entry: TimelineEntry, users: LightUserMap) {
function renderForum(entry: ForumPostTimelineEntry, users: LightUserMap) {
const data = entry.data
const actor = users[data.userId]
return h('li.list_item.timelineEntry', {
key: 'forum-post' + data.postId,
'data-external': `/forum/redirect/post/${data.postId}`,
}, [
userTitle(false, actor.patron ?? false, actor.id, actor.title),
h.trust(i18n('xPostedInForumY', '', `<strong>${data.topicName}</strong>`)),
i18nVdom('xPostedInForumY', '', h('strong', data.topicName)),
' ',
h('small', h('em', entry.fromNow)),
])
}

function renderStudy(entry: TimelineEntry) {
function renderStudy(entry: StudyCreateTimelineEntry | StudyLikeTimelineEntry) {
const data = entry.data
const eType = entry.type === 'study-create' ? 'hosts' : 'likes'
return h('li.list_item.timelineEntry', {
Expand All @@ -161,37 +161,37 @@ function renderStudy(entry: TimelineEntry) {
])
}

function renderTourJoin(entry: TimelineEntry) {
const entryText = i18n('xCompetesInY', entry.data.userId, entry.data.tourName)
function renderTourJoin(entry: TourJoinTimelineEntry) {
const entryText = i18nVdom('xCompetesInY', h('strong', entry.data.userId), entry.data.tourName)
const key = 'tour' + entry.date

return (
<li className="list_item timelineEntry" key={key}
data-path={`/tournament/${entry.data.tourId}`}
>
<span className="fa fa-trophy" />
{h.trust(entryText.replace(/^(\w+)\s/, '<strong>$1&nbsp;</strong>'))}
{entryText}
<small><em> {entry.fromNow}</em></small>
</li>
)
}

function renderFollow(entry: TimelineEntry) {
const entryText = i18n('xStartedFollowingY', entry.data.u1, entry.data.u2)
function renderFollow(entry: FollowTimelineEntry) {
const entryText = i18nVdom('xStartedFollowingY', h('strong', entry.data.u1), entry.data.u2)
const key = 'follow' + entry.date

return (
<li className="list_item timelineEntry" key={key}
data-path={`/@/${entry.data.u2}`}
>
<span className="fa fa-arrow-circle-right" />
{h.trust(entryText.replace(/^(\w+)\s/, '<strong>$1&nbsp;</strong>'))}
{entryText}
<small><em> {entry.fromNow}</em></small>
</li>
)
}

function renderGameEnd(entry: TimelineEntry) {
function renderGameEnd(entry: GameEndTimelineEntry) {
const icon = gameIcon(entry.data.perf)
const result = typeof entry.data.win === 'undefined' ? i18n('draw') : (entry.data.win ? 'Victory' : 'Defeat')
const key = 'game-end' + entry.date
Expand All @@ -206,7 +206,7 @@ function renderGameEnd(entry: TimelineEntry) {
)
}

function renderStreamStart(entry: TimelineEntry) {
function renderStreamStart(entry: StreamStartTimelineEntry) {
const data = entry.data
return h('li.list_item.timelineEntry', {
key: `stream-start${data.date}`,
Expand Down