mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 17:43:47 +00:00
Add files via upload
This commit is contained in:
@@ -26,6 +26,19 @@ const appSchemes = [
|
|||||||
|
|
||||||
const COUNTDOWN_SECONDS = 5
|
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() {
|
export default function DeepLinkRedirect() {
|
||||||
const { i18n } = useTranslation()
|
const { i18n } = useTranslation()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
@@ -104,13 +117,13 @@ export default function DeepLinkRedirect() {
|
|||||||
|
|
||||||
// Open deep link - same as miniapp, just window.location.href
|
// Open deep link - same as miniapp, just window.location.href
|
||||||
const openDeepLink = useCallback(() => {
|
const openDeepLink = useCallback(() => {
|
||||||
if (!deepLink) return
|
if (!deepLink || !isValidDeepLink(deepLink)) return
|
||||||
window.location.href = deepLink
|
window.location.href = deepLink
|
||||||
}, [deepLink])
|
}, [deepLink])
|
||||||
|
|
||||||
// Countdown timer effect
|
// Countdown timer effect
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!deepLink) {
|
if (!deepLink || !isValidDeepLink(deepLink)) {
|
||||||
setStatus('error')
|
setStatus('error')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import DOMPurify from 'dompurify'
|
||||||
import { infoApi, FaqPage } from '../api/info'
|
import { infoApi, FaqPage } from '../api/info'
|
||||||
|
|
||||||
const InfoIcon = () => (
|
const InfoIcon = () => (
|
||||||
@@ -41,6 +42,15 @@ const ChevronIcon = ({ expanded }: { expanded: boolean }) => (
|
|||||||
|
|
||||||
type TabType = 'faq' | 'rules' | 'privacy' | 'offer'
|
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
|
// Convert plain text to HTML with proper formatting
|
||||||
const formatContent = (content: string): string => {
|
const formatContent = (content: string): string => {
|
||||||
if (!content) return ''
|
if (!content) return ''
|
||||||
@@ -49,11 +59,11 @@ const formatContent = (content: string): string => {
|
|||||||
const hasHtmlTags = /<[a-z][\s\S]*>/i.test(content)
|
const hasHtmlTags = /<[a-z][\s\S]*>/i.test(content)
|
||||||
|
|
||||||
if (hasHtmlTags) {
|
if (hasHtmlTags) {
|
||||||
return content
|
return sanitizeHtml(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert plain text to formatted HTML
|
// Convert plain text to formatted HTML
|
||||||
return content
|
const result = content
|
||||||
.split(/\n\n+/) // Split by double newlines (paragraphs)
|
.split(/\n\n+/) // Split by double newlines (paragraphs)
|
||||||
.map(paragraph => {
|
.map(paragraph => {
|
||||||
// Check if it's a header (starts with # or numeric like "1.")
|
// 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>`
|
return `<p>${formattedParagraph}</p>`
|
||||||
})
|
})
|
||||||
.join('')
|
.join('')
|
||||||
|
|
||||||
|
return sanitizeHtml(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Info() {
|
export default function Info() {
|
||||||
|
|||||||
@@ -5,6 +5,26 @@ import { useQuery } from '@tanstack/react-query'
|
|||||||
import { useAuthStore } from '../store/auth'
|
import { useAuthStore } from '../store/auth'
|
||||||
import { brandingApi } from '../api/branding'
|
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() {
|
export default function TelegramRedirect() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
@@ -24,8 +44,8 @@ export default function TelegramRedirect() {
|
|||||||
const logoLetter = branding?.logo_letter || import.meta.env.VITE_APP_LOGO || 'V'
|
const logoLetter = branding?.logo_letter || import.meta.env.VITE_APP_LOGO || 'V'
|
||||||
const logoUrl = branding ? brandingApi.getLogoUrl(branding) : null
|
const logoUrl = branding ? brandingApi.getLogoUrl(branding) : null
|
||||||
|
|
||||||
// Get redirect target from URL params
|
// Get redirect target from URL params (validated)
|
||||||
const redirectTo = searchParams.get('redirect') || '/'
|
const redirectTo = getSafeRedirectUrl(searchParams.get('redirect'))
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// If already authenticated, redirect immediately
|
// If already authenticated, redirect immediately
|
||||||
|
|||||||
Reference in New Issue
Block a user