mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-08-02 07:45:40 +00:00
fix(web): repair 6 dead internal links + harden forum markdown against stored XSS
Routing: footer "Explorer"/"Validators" pointed to non-existent /network (4 spots in AppLayout + LandingPageDesktop) and Explorer/Forum quick-links pointed to bare /governance (no route) -> all hit the 404 catch-all. Repointed to /explorer and /governance/assembly; footer "Vote" /-> /elections. Security (stored XSS): DiscussionThread.parseMarkdown did regex->HTML then dangerouslySetInnerHTML with no escaping/sanitizer, so <img onerror>/javascript: links in user comments executed. Now escape HTML first, then DOMPurify-sanitize the output to a safe tag/attr allow-list with http(s)-only hrefs.
This commit is contained in:
@@ -659,9 +659,9 @@ const AppLayout: React.FC = () => {
|
|||||||
<div className="lp-foot-col">
|
<div className="lp-foot-col">
|
||||||
<h5>{t('landing.footer.network')}</h5>
|
<h5>{t('landing.footer.network')}</h5>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="/network">{t('landing.footer.explorer')}</a></li>
|
<li><a href="/explorer">{t('landing.footer.explorer')}</a></li>
|
||||||
<li><a href="/telemetry">{t('landing.footer.telemetry')}</a></li>
|
<li><a href="/telemetry">{t('landing.footer.telemetry')}</a></li>
|
||||||
<li><a href="/network">{t('landing.footer.validators')}</a></li>
|
<li><a href="/explorer">{t('landing.footer.validators')}</a></li>
|
||||||
<li><a href="/faucet">{t('landing.footer.faucet')}</a></li>
|
<li><a href="/faucet">{t('landing.footer.faucet')}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -670,7 +670,7 @@ const AppLayout: React.FC = () => {
|
|||||||
<ul>
|
<ul>
|
||||||
<li><a href="/wallet">{t('landing.footer.wallet')}</a></li>
|
<li><a href="/wallet">{t('landing.footer.wallet')}</a></li>
|
||||||
<li><a href="/p2p">{t('landing.footer.trade')}</a></li>
|
<li><a href="/p2p">{t('landing.footer.trade')}</a></li>
|
||||||
<li><a href="/">{t('landing.footer.vote')}</a></li>
|
<li><a href="/elections">{t('landing.footer.vote')}</a></li>
|
||||||
<li><a href="/grants">{t('landing.footer.grants')}</a></li>
|
<li><a href="/grants">{t('landing.footer.grants')}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -689,7 +689,7 @@ const AppLayout: React.FC = () => {
|
|||||||
<li><a href="/forum">{t('landing.footer.forum')}</a></li>
|
<li><a href="/forum">{t('landing.footer.forum')}</a></li>
|
||||||
<li><a href="https://discord.gg/pezkuwichain" target="_blank" rel="noopener noreferrer">{t('landing.footer.discord')}</a></li>
|
<li><a href="https://discord.gg/pezkuwichain" target="_blank" rel="noopener noreferrer">{t('landing.footer.discord')}</a></li>
|
||||||
<li><a href="https://t.me/PezkuwiApp" target="_blank" rel="noopener noreferrer">{t('landing.footer.telegram')}</a></li>
|
<li><a href="https://t.me/PezkuwiApp" target="_blank" rel="noopener noreferrer">{t('landing.footer.telegram')}</a></li>
|
||||||
<li><a href="https://x.com/PezkuwiChain" target="_blank" rel="noopener noreferrer">{t('landing.footer.twitter')}</a></li>
|
<li><a href="https://x.com/bizinikiwi" target="_blank" rel="noopener noreferrer">{t('landing.footer.twitter')}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
|
||||||
import { useWebSocket } from '@/contexts/WebSocketContext';
|
import { useWebSocket } from '@/contexts/WebSocketContext';
|
||||||
import { useToast } from '@/hooks/use-toast';
|
import { useToast } from '@/hooks/use-toast';
|
||||||
|
import DOMPurify from 'dompurify';
|
||||||
|
|
||||||
interface Comment {
|
interface Comment {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -329,8 +330,18 @@ export function DiscussionThread({ proposalId }: { proposalId: string }) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// SECURITY: user-supplied comment content. Escape all HTML FIRST so raw markup
|
||||||
|
// (e.g. <img src=x onerror=...>) becomes inert text, THEN apply the markdown
|
||||||
|
// transforms, THEN run the result through DOMPurify restricted to a safe tag/attr
|
||||||
|
// allow-list with http(s)-only hrefs. This blocks stored XSS (script/onerror/
|
||||||
|
// javascript: URLs) while keeping basic markdown rendering.
|
||||||
const parseMarkdown = (text: string): string => {
|
const parseMarkdown = (text: string): string => {
|
||||||
return text
|
const escaped = (text || '')
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"');
|
||||||
|
const html = escaped
|
||||||
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
|
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
|
||||||
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
|
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
|
||||||
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
|
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
|
||||||
@@ -339,6 +350,11 @@ export function DiscussionThread({ proposalId }: { proposalId: string }) {
|
|||||||
.replace(/\[([^\]]+)\]\(([^)]+)\)/gim, '<a href="$2" class="text-blue-600 hover:underline">$1</a>')
|
.replace(/\[([^\]]+)\]\(([^)]+)\)/gim, '<a href="$2" class="text-blue-600 hover:underline">$1</a>')
|
||||||
.replace(/^> (.*$)/gim, '<blockquote class="border-l-4 border-gray-300 pl-4 italic">$1</blockquote>')
|
.replace(/^> (.*$)/gim, '<blockquote class="border-l-4 border-gray-300 pl-4 italic">$1</blockquote>')
|
||||||
.replace(/\n/gim, '<br>');
|
.replace(/\n/gim, '<br>');
|
||||||
|
return DOMPurify.sanitize(html, {
|
||||||
|
ALLOWED_TAGS: ['h1', 'h2', 'h3', 'strong', 'em', 'a', 'blockquote', 'br', 'p'],
|
||||||
|
ALLOWED_ATTR: ['href', 'class'],
|
||||||
|
ALLOWED_URI_REGEXP: /^https?:\/\//i,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1148,9 +1148,9 @@ const LandingPageDesktop: React.FC = () => {
|
|||||||
<div className="lp-foot-col">
|
<div className="lp-foot-col">
|
||||||
<h5>{t('landing.footer.network')}</h5>
|
<h5>{t('landing.footer.network')}</h5>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="/network">{t('landing.footer.explorer')}</a></li>
|
<li><a href="/explorer">{t('landing.footer.explorer')}</a></li>
|
||||||
<li><a href="/telemetry">{t('landing.footer.telemetry')}</a></li>
|
<li><a href="/telemetry">{t('landing.footer.telemetry')}</a></li>
|
||||||
<li><a href="/network">{t('landing.footer.validators')}</a></li>
|
<li><a href="/explorer">{t('landing.footer.validators')}</a></li>
|
||||||
<li><a href="/faucet">{t('landing.footer.faucet')}</a></li>
|
<li><a href="/faucet">{t('landing.footer.faucet')}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -1159,7 +1159,7 @@ const LandingPageDesktop: React.FC = () => {
|
|||||||
<ul>
|
<ul>
|
||||||
<li><a href="/wallet">{t('landing.footer.wallet')}</a></li>
|
<li><a href="/wallet">{t('landing.footer.wallet')}</a></li>
|
||||||
<li><a href="/p2p">{t('landing.footer.trade')}</a></li>
|
<li><a href="/p2p">{t('landing.footer.trade')}</a></li>
|
||||||
<li><a href="/">{t('landing.footer.vote')}</a></li>
|
<li><a href="/elections">{t('landing.footer.vote')}</a></li>
|
||||||
<li><a href="/grants">{t('landing.footer.grants')}</a></li>
|
<li><a href="/grants">{t('landing.footer.grants')}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -1178,7 +1178,7 @@ const LandingPageDesktop: React.FC = () => {
|
|||||||
<li><a href="/forum">{t('landing.footer.forum')}</a></li>
|
<li><a href="/forum">{t('landing.footer.forum')}</a></li>
|
||||||
<li><a href="https://discord.gg/pezkuwichain" target="_blank" rel="noopener noreferrer">{t('landing.footer.discord')}</a></li>
|
<li><a href="https://discord.gg/pezkuwichain" target="_blank" rel="noopener noreferrer">{t('landing.footer.discord')}</a></li>
|
||||||
<li><a href="https://t.me/PezkuwiApp" target="_blank" rel="noopener noreferrer">{t('landing.footer.telegram')}</a></li>
|
<li><a href="https://t.me/PezkuwiApp" target="_blank" rel="noopener noreferrer">{t('landing.footer.telegram')}</a></li>
|
||||||
<li><a href="https://x.com/PezkuwiChain" target="_blank" rel="noopener noreferrer">{t('landing.footer.twitter')}</a></li>
|
<li><a href="https://x.com/bizinikiwi" target="_blank" rel="noopener noreferrer">{t('landing.footer.twitter')}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -860,7 +860,7 @@ const Explorer: React.FC = () => {
|
|||||||
{t('explorer.telemetry')}
|
{t('explorer.telemetry')}
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="/governance"
|
href="/governance/assembly"
|
||||||
className="flex items-center gap-2 p-3 rounded-lg bg-gray-800 hover:bg-gray-750 text-gray-300 hover:text-white transition-colors"
|
className="flex items-center gap-2 p-3 rounded-lg bg-gray-800 hover:bg-gray-750 text-gray-300 hover:text-white transition-colors"
|
||||||
>
|
>
|
||||||
<Users className="w-4 h-4 text-blue-400" />
|
<Users className="w-4 h-4 text-blue-400" />
|
||||||
|
|||||||
@@ -429,7 +429,7 @@ const Forum: React.FC = () => {
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-2">
|
<CardContent className="space-y-2">
|
||||||
<a href="/governance" className="block text-gray-400 hover:text-green-400 text-sm transition-colors">
|
<a href="/governance/assembly" className="block text-gray-400 hover:text-green-400 text-sm transition-colors">
|
||||||
→ {t('forum.govDashboard')}
|
→ {t('forum.govDashboard')}
|
||||||
</a>
|
</a>
|
||||||
<a href="/docs" className="block text-gray-400 hover:text-green-400 text-sm transition-colors">
|
<a href="/docs" className="block text-gray-400 hover:text-green-400 text-sm transition-colors">
|
||||||
|
|||||||
Reference in New Issue
Block a user