Add files via upload

This commit is contained in:
Egor
2026-01-18 06:20:11 +03:00
committed by GitHub
parent 6baa573e4b
commit 8be393bf63
3 changed files with 51 additions and 6 deletions

View File

@@ -26,6 +26,19 @@ const appSchemes = [
const COUNTDOWN_SECONDS = 5
// Validate deep link to prevent javascript: and other dangerous schemes
const isValidDeepLink = (url: string): boolean => {
if (!url) return false
const lowerUrl = url.toLowerCase().trim()
// Block dangerous schemes
const dangerousSchemes = ['javascript:', 'data:', 'vbscript:', 'file:']
if (dangerousSchemes.some(scheme => lowerUrl.startsWith(scheme))) {
return false
}
// Only allow known app schemes
return appSchemes.some(app => lowerUrl.startsWith(app.scheme))
}
export default function DeepLinkRedirect() {
const { i18n } = useTranslation()
const navigate = useNavigate()
@@ -104,13 +117,13 @@ export default function DeepLinkRedirect() {
// Open deep link - same as miniapp, just window.location.href
const openDeepLink = useCallback(() => {
if (!deepLink) return
if (!deepLink || !isValidDeepLink(deepLink)) return
window.location.href = deepLink
}, [deepLink])
// Countdown timer effect
useEffect(() => {
if (!deepLink) {
if (!deepLink || !isValidDeepLink(deepLink)) {
setStatus('error')
return
}

View File

@@ -1,6 +1,7 @@
import { useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next'
import DOMPurify from 'dompurify'
import { infoApi, FaqPage } from '../api/info'
const InfoIcon = () => (
@@ -41,6 +42,15 @@ const ChevronIcon = ({ expanded }: { expanded: boolean }) => (
type TabType = 'faq' | 'rules' | 'privacy' | 'offer'
// Sanitize HTML content to prevent XSS
const sanitizeHtml = (html: string): string => {
return DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['p', 'br', 'b', 'i', 'u', 'strong', 'em', 'a', 'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'code', 'pre', 'span', 'div'],
ALLOWED_ATTR: ['href', 'target', 'rel', 'class'],
ALLOW_DATA_ATTR: false,
})
}
// Convert plain text to HTML with proper formatting
const formatContent = (content: string): string => {
if (!content) return ''
@@ -49,11 +59,11 @@ const formatContent = (content: string): string => {
const hasHtmlTags = /<[a-z][\s\S]*>/i.test(content)
if (hasHtmlTags) {
return content
return sanitizeHtml(content)
}
// Convert plain text to formatted HTML
return content
const result = content
.split(/\n\n+/) // Split by double newlines (paragraphs)
.map(paragraph => {
// Check if it's a header (starts with # or numeric like "1.")
@@ -83,6 +93,8 @@ const formatContent = (content: string): string => {
return `<p>${formattedParagraph}</p>`
})
.join('')
return sanitizeHtml(result)
}
export default function Info() {

View File

@@ -5,6 +5,26 @@ import { useQuery } from '@tanstack/react-query'
import { useAuthStore } from '../store/auth'
import { brandingApi } from '../api/branding'
// Validate redirect URL to prevent open redirect attacks
const getSafeRedirectUrl = (url: string | null): string => {
if (!url) return '/'
// Only allow relative paths starting with /
// Block protocol-relative URLs (//evil.com) and absolute URLs
if (!url.startsWith('/') || url.startsWith('//')) {
return '/'
}
// Additional check for encoded characters that could bypass validation
try {
const decoded = decodeURIComponent(url)
if (!decoded.startsWith('/') || decoded.startsWith('//') || decoded.includes('://')) {
return '/'
}
} catch {
return '/'
}
return url
}
export default function TelegramRedirect() {
const { t } = useTranslation()
const navigate = useNavigate()
@@ -24,8 +44,8 @@ export default function TelegramRedirect() {
const logoLetter = branding?.logo_letter || import.meta.env.VITE_APP_LOGO || 'V'
const logoUrl = branding ? brandingApi.getLogoUrl(branding) : null
// Get redirect target from URL params
const redirectTo = searchParams.get('redirect') || '/'
// Get redirect target from URL params (validated)
const redirectTo = getSafeRedirectUrl(searchParams.get('redirect'))
useEffect(() => {
// If already authenticated, redirect immediately