diff --git a/Makefile b/Makefile index 9de826b99..4c568bf3d 100644 --- a/Makefile +++ b/Makefile @@ -8,9 +8,8 @@ GEN_SERVER_DIR := src/generated/server DOCS_API_DIR := docs/api # Go install settings -GO_PROXY := GOPROXY=direct GO_PRIVATE := GOPRIVATE=github.com/SebastienMelki -GO_INSTALL := $(GO_PROXY) $(GO_PRIVATE) go install +GO_INSTALL := $(GO_PRIVATE) go install # Required tool versions BUF_VERSION := v1.64.0 diff --git a/api/[domain]/v1/[rpc].ts b/api/[domain]/v1/[rpc].ts index 7ed18373e..0a0a065b3 100644 --- a/api/[domain]/v1/[rpc].ts +++ b/api/[domain]/v1/[rpc].ts @@ -86,7 +86,7 @@ export default async function handler(request: Request): Promise { try { corsHeaders = getCorsHeaders(request); } catch { - corsHeaders = { 'Access-Control-Allow-Origin': '*' }; + corsHeaders = { 'Access-Control-Allow-Origin': 'https://worldmonitor.app' }; } // OPTIONS preflight diff --git a/api/download.js b/api/download.js index 36db3df88..cea9c1609 100644 --- a/api/download.js +++ b/api/download.js @@ -64,10 +64,16 @@ export default async function handler(req) { return Response.redirect(RELEASES_PAGE, 302); } + // Validate redirect URL is a GitHub download + const downloadUrl = String(asset.browser_download_url || ''); + if (!downloadUrl.startsWith('https://github.com/')) { + return Response.redirect(RELEASES_PAGE, 302); + } + return new Response(null, { status: 302, headers: { - 'Location': asset.browser_download_url, + 'Location': downloadUrl, 'Cache-Control': 'public, s-maxage=300, stale-while-revalidate=60', }, }); diff --git a/api/rss-proxy.js b/api/rss-proxy.js index 317b7bc07..6f8ff7b71 100644 --- a/api/rss-proxy.js +++ b/api/rss-proxy.js @@ -15,185 +15,10 @@ async function fetchWithTimeout(url, options, timeoutMs = 15000) { } } -// Allowed RSS feed domains for security -const ALLOWED_DOMAINS = [ - 'feeds.bbci.co.uk', - 'www.theguardian.com', - 'feeds.npr.org', - 'news.google.com', - 'www.aljazeera.com', - 'rss.cnn.com', - 'hnrss.org', - 'feeds.arstechnica.com', - 'www.theverge.com', - 'www.cnbc.com', - 'feeds.marketwatch.com', - 'www.defenseone.com', - 'breakingdefense.com', - 'www.bellingcat.com', - 'techcrunch.com', - 'huggingface.co', - 'www.technologyreview.com', - 'rss.arxiv.org', - 'export.arxiv.org', - 'www.federalreserve.gov', - 'www.sec.gov', - 'www.whitehouse.gov', - 'www.state.gov', - 'www.defense.gov', - 'home.treasury.gov', - 'www.justice.gov', - 'tools.cdc.gov', - 'www.fema.gov', - 'www.dhs.gov', - 'www.thedrive.com', - 'krebsonsecurity.com', - 'finance.yahoo.com', - 'thediplomat.com', - 'venturebeat.com', - 'foreignpolicy.com', - 'www.ft.com', - 'openai.com', - 'www.reutersagency.com', - 'feeds.reuters.com', - 'rsshub.app', - 'asia.nikkei.com', - 'www.cfr.org', - 'www.csis.org', - 'www.politico.com', - 'www.brookings.edu', - 'layoffs.fyi', - 'www.defensenews.com', - 'www.militarytimes.com', - 'taskandpurpose.com', - 'news.usni.org', - 'www.oryxspioenkop.com', - 'www.gov.uk', - 'www.foreignaffairs.com', - 'www.atlanticcouncil.org', - // Tech variant domains - 'www.zdnet.com', - 'www.techmeme.com', - 'www.darkreading.com', - 'www.schneier.com', - 'rss.politico.com', - 'www.anandtech.com', - 'www.tomshardware.com', - 'www.semianalysis.com', - 'feed.infoq.com', - 'thenewstack.io', - 'devops.com', - 'dev.to', - 'lobste.rs', - 'changelog.com', - 'seekingalpha.com', - 'news.crunchbase.com', - 'www.saastr.com', - 'feeds.feedburner.com', - // Additional tech variant domains - 'www.producthunt.com', - 'www.axios.com', - 'github.blog', - 'githubnext.com', - 'mshibanami.github.io', - 'www.engadget.com', - 'news.mit.edu', - 'dev.events', - // VC blogs - 'www.ycombinator.com', - 'a16z.com', - 'review.firstround.com', - 'www.sequoiacap.com', - 'www.nfx.com', - 'www.aaronsw.com', - 'bothsidesofthetable.com', - 'www.lennysnewsletter.com', - 'stratechery.com', - // Regional startup news - 'www.eu-startups.com', - 'tech.eu', - 'sifted.eu', - 'www.techinasia.com', - 'kr-asia.com', - 'techcabal.com', - 'disrupt-africa.com', - 'lavca.org', - 'contxto.com', - 'inc42.com', - 'yourstory.com', - // Funding & VC - 'pitchbook.com', - 'www.cbinsights.com', - // Accelerators - 'www.techstars.com', - // Middle East & Regional News - 'english.alarabiya.net', - 'www.arabnews.com', - 'www.timesofisrael.com', - 'www.haaretz.com', - 'www.scmp.com', - 'kyivindependent.com', - 'www.themoscowtimes.com', - 'feeds.24.com', - 'feeds.capi24.com', // News24 redirect destination - // International News Sources - 'www.france24.com', - 'www.euronews.com', - 'www.lemonde.fr', - 'rss.dw.com', - 'www.africanews.com', - 'www.lasillavacia.com', - 'www.channelnewsasia.com', - 'www.thehindu.com', - // International Organizations - 'news.un.org', - 'www.iaea.org', - 'www.who.int', - 'www.cisa.gov', - 'www.crisisgroup.org', - // Think Tanks & Research (Added 2026-01-29) - 'rusi.org', - 'warontherocks.com', - 'www.aei.org', - 'responsiblestatecraft.org', - 'www.fpri.org', - 'jamestown.org', - 'www.chathamhouse.org', - 'ecfr.eu', - 'www.gmfus.org', - 'www.wilsoncenter.org', - 'www.lowyinstitute.org', - 'www.mei.edu', - 'www.stimson.org', - 'www.cnas.org', - 'carnegieendowment.org', - 'www.rand.org', - 'fas.org', - 'www.armscontrol.org', - 'www.nti.org', - 'thebulletin.org', - 'www.iss.europa.eu', - // Economic & Food Security - 'www.fao.org', - 'worldbank.org', - 'www.imf.org', - // Regional locale feeds (tr, pl, ru, th, vi) - 'www.hurriyet.com.tr', - 'tvn24.pl', - 'www.polsatnews.pl', - 'www.rp.pl', - 'meduza.io', - 'novayagazeta.eu', - 'www.bangkokpost.com', - 'vnexpress.net', - 'www.abc.net.au', - // Additional - 'news.ycombinator.com', - // Finance variant - 'seekingalpha.com', - 'www.coindesk.com', - 'cointelegraph.com', -]; +// Allowed RSS feed domains — single source of truth: data/rss-allowed-domains.json +// KEEP IN SYNC with src-tauri/sidecar/local-api-server.mjs (reads the same JSON at build time) +import allowedDomains from '../data/rss-allowed-domains.json'; +const ALLOWED_DOMAINS = allowedDomains; export default async function handler(req) { const corsHeaders = getCorsHeaders(req, 'GET, OPTIONS'); diff --git a/api/version.js b/api/version.js index bfa4975f0..4543e19c1 100644 --- a/api/version.js +++ b/api/version.js @@ -33,7 +33,7 @@ export default async function handler() { headers: { 'Content-Type': 'application/json', 'Cache-Control': 'public, s-maxage=300, stale-while-revalidate=60', - 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Origin': 'https://worldmonitor.app', }, }); } catch { diff --git a/api/youtube/embed.js b/api/youtube/embed.js index fcfbd37d4..68f90f1f9 100644 --- a/api/youtube/embed.js +++ b/api/youtube/embed.js @@ -13,7 +13,6 @@ function sanitizeVideoId(value) { const ALLOWED_ORIGINS = [ /^https:\/\/(.*\.)?worldmonitor\.app$/, /^https:\/\/worldmonitor-[a-z0-9-]+-elie-habib-projects\.vercel\.app$/, - /^https:\/\/worldmonitor-[a-z0-9-]+\.vercel\.app$/, /^https?:\/\/localhost(:\d+)?$/, /^https?:\/\/127\.0\.0\.1(:\d+)?$/, /^tauri:\/\/localhost$/, @@ -88,12 +87,12 @@ export default async function handler(request) { playerVars:{autoplay:${autoplay},mute:${mute},playsinline:1,rel:0,controls:1,modestbranding:1,enablejsapi:1,origin:${JSON.stringify(origin)},widget_referrer:${JSON.stringify(origin)}}, events:{ onReady:function(){ - window.parent.postMessage({type:'yt-ready'},'*'); + window.parent.postMessage({type:'yt-ready'},${JSON.stringify(origin)}); if(${autoplay}===1){player.playVideo()} }, - onError:function(e){window.parent.postMessage({type:'yt-error',code:e.data},'*')}, + onError:function(e){window.parent.postMessage({type:'yt-error',code:e.data},${JSON.stringify(origin)})}, onStateChange:function(e){ - window.parent.postMessage({type:'yt-state',state:e.data},'*'); + window.parent.postMessage({type:'yt-state',state:e.data},${JSON.stringify(origin)}); if(e.data===1||e.data===3){hideOverlay();started=true} } } @@ -103,7 +102,9 @@ export default async function handler(request) { if(player&&player.playVideo){player.playVideo();player.unMute();hideOverlay()} }); setTimeout(function(){if(!started)overlay.classList.remove('hidden')},3000); + var allowedOrigin=${JSON.stringify(origin)}; window.addEventListener('message',function(e){ + if(e.origin!==allowedOrigin)return; if(!player||!player.getPlayerState)return; var m=e.data;if(!m||!m.type)return; switch(m.type){ diff --git a/api/youtube/live.js b/api/youtube/live.js index cc6afed49..7ce270f1d 100644 --- a/api/youtube/live.js +++ b/api/youtube/live.js @@ -23,6 +23,14 @@ export default async function handler(request) { }); } + // Validate channel parameter to prevent path traversal + if (!/^@?[A-Za-z0-9_.\-]{1,100}$/.test(channel)) { + return new Response(JSON.stringify({ error: 'Invalid channel parameter' }), { + status: 400, + headers: { 'Content-Type': 'application/json' }, + }); + } + try { // Try to fetch the channel's live page const channelHandle = channel.startsWith('@') ? channel : `@${channel}`; diff --git a/data/rss-allowed-domains.json b/data/rss-allowed-domains.json new file mode 100644 index 000000000..bdaf521cc --- /dev/null +++ b/data/rss-allowed-domains.json @@ -0,0 +1,163 @@ +[ + "feeds.bbci.co.uk", + "www.theguardian.com", + "feeds.npr.org", + "news.google.com", + "www.aljazeera.com", + "rss.cnn.com", + "hnrss.org", + "feeds.arstechnica.com", + "www.theverge.com", + "www.cnbc.com", + "feeds.marketwatch.com", + "www.defenseone.com", + "breakingdefense.com", + "www.bellingcat.com", + "techcrunch.com", + "huggingface.co", + "www.technologyreview.com", + "rss.arxiv.org", + "export.arxiv.org", + "www.federalreserve.gov", + "www.sec.gov", + "www.whitehouse.gov", + "www.state.gov", + "www.defense.gov", + "home.treasury.gov", + "www.justice.gov", + "tools.cdc.gov", + "www.fema.gov", + "www.dhs.gov", + "www.thedrive.com", + "krebsonsecurity.com", + "finance.yahoo.com", + "thediplomat.com", + "venturebeat.com", + "foreignpolicy.com", + "www.ft.com", + "openai.com", + "www.reutersagency.com", + "feeds.reuters.com", + "rsshub.app", + "asia.nikkei.com", + "www.cfr.org", + "www.csis.org", + "www.politico.com", + "www.brookings.edu", + "layoffs.fyi", + "www.defensenews.com", + "www.militarytimes.com", + "taskandpurpose.com", + "news.usni.org", + "www.oryxspioenkop.com", + "www.gov.uk", + "www.foreignaffairs.com", + "www.atlanticcouncil.org", + "www.zdnet.com", + "www.techmeme.com", + "www.darkreading.com", + "www.schneier.com", + "rss.politico.com", + "www.anandtech.com", + "www.tomshardware.com", + "www.semianalysis.com", + "feed.infoq.com", + "thenewstack.io", + "devops.com", + "dev.to", + "lobste.rs", + "changelog.com", + "seekingalpha.com", + "news.crunchbase.com", + "www.saastr.com", + "feeds.feedburner.com", + "www.producthunt.com", + "www.axios.com", + "github.blog", + "githubnext.com", + "mshibanami.github.io", + "www.engadget.com", + "news.mit.edu", + "dev.events", + "www.ycombinator.com", + "a16z.com", + "review.firstround.com", + "www.sequoiacap.com", + "www.nfx.com", + "www.aaronsw.com", + "bothsidesofthetable.com", + "www.lennysnewsletter.com", + "stratechery.com", + "www.eu-startups.com", + "tech.eu", + "sifted.eu", + "www.techinasia.com", + "kr-asia.com", + "techcabal.com", + "disrupt-africa.com", + "lavca.org", + "contxto.com", + "inc42.com", + "yourstory.com", + "pitchbook.com", + "www.cbinsights.com", + "www.techstars.com", + "english.alarabiya.net", + "www.arabnews.com", + "www.timesofisrael.com", + "www.haaretz.com", + "www.scmp.com", + "kyivindependent.com", + "www.themoscowtimes.com", + "feeds.24.com", + "feeds.capi24.com", + "www.france24.com", + "www.euronews.com", + "www.lemonde.fr", + "rss.dw.com", + "www.africanews.com", + "www.lasillavacia.com", + "www.channelnewsasia.com", + "www.thehindu.com", + "news.un.org", + "www.iaea.org", + "www.who.int", + "www.cisa.gov", + "www.crisisgroup.org", + "rusi.org", + "warontherocks.com", + "www.aei.org", + "responsiblestatecraft.org", + "www.fpri.org", + "jamestown.org", + "www.chathamhouse.org", + "ecfr.eu", + "www.gmfus.org", + "www.wilsoncenter.org", + "www.lowyinstitute.org", + "www.mei.edu", + "www.stimson.org", + "www.cnas.org", + "carnegieendowment.org", + "www.rand.org", + "fas.org", + "www.armscontrol.org", + "www.nti.org", + "thebulletin.org", + "www.iss.europa.eu", + "www.fao.org", + "worldbank.org", + "www.imf.org", + "www.hurriyet.com.tr", + "tvn24.pl", + "www.polsatnews.pl", + "www.rp.pl", + "meduza.io", + "novayagazeta.eu", + "www.bangkokpost.com", + "vnexpress.net", + "www.abc.net.au", + "news.ycombinator.com", + "www.coindesk.com", + "cointelegraph.com" +] diff --git a/server/worldmonitor/research/v1/list-arxiv-papers.ts b/server/worldmonitor/research/v1/list-arxiv-papers.ts index bf69940bd..5d2570770 100644 --- a/server/worldmonitor/research/v1/list-arxiv-papers.ts +++ b/server/worldmonitor/research/v1/list-arxiv-papers.ts @@ -30,12 +30,17 @@ async function fetchArxivPapers(req: ListArxivPapersRequest): Promise) -> Result<(), String> { } fn generate_local_token() -> String { - use std::collections::hash_map::RandomState; - use std::hash::{BuildHasher, Hasher}; - let state = RandomState::new(); - let mut h1 = state.build_hasher(); - h1.write_u64(std::process::id() as u64); - let a = h1.finish(); - let mut h2 = state.build_hasher(); - let nanos = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|d| d.as_nanos()) - .unwrap_or(0); - h2.write_u128(nanos); - let b = h2.finish(); - format!("{a:016x}{b:016x}") + // Use OS CSPRNG for cryptographically secure token generation (cross-platform) + let mut buf = [0u8; 32]; + getrandom::getrandom(&mut buf).expect("OS CSPRNG failed"); + buf.iter().map(|b| format!("{b:02x}")).collect() } #[tauri::command] diff --git a/src/components/LiveNewsPanel.ts b/src/components/LiveNewsPanel.ts index db080ab69..1d9ea34a8 100644 --- a/src/components/LiveNewsPanel.ts +++ b/src/components/LiveNewsPanel.ts @@ -372,9 +372,13 @@ export class LiveNewsPanel extends Panel {
📺
${t('components.liveNews.notLive', { name: channel.name })}
- +
`; + const retryBtn = this.content.querySelector('.offline-retry'); + retryBtn?.addEventListener('click', () => { + (this.content.closest('.panel')?.querySelector('.live-channel-btn.active') as HTMLElement)?.click(); + }); } private showEmbedError(channel: LiveChannel, errorCode: number): void { @@ -386,9 +390,11 @@ export class LiveNewsPanel extends Panel {
!
${t('components.liveNews.cannotEmbed', { name: channel.name, code: String(errorCode) })}
- ${t('components.liveNews.openOnYouTube')} + ${t('components.liveNews.openOnYouTube')}
`; + const link = this.content.querySelector('.offline-retry') as HTMLAnchorElement | null; + if (link) link.href = watchUrl; } private renderPlayer(): void { diff --git a/src/settings-main.ts b/src/settings-main.ts index 3cb45c6e8..8dfc095cf 100644 --- a/src/settings-main.ts +++ b/src/settings-main.ts @@ -219,7 +219,7 @@ function initDiagnostics(): void { const rows = entries.slice().reverse().map((e) => { const ts = e.timestamp.split('T')[1]?.replace('Z', '') || e.timestamp; const cls = e.status < 300 ? 'ok' : e.status < 500 ? 'warn' : 'err'; - return `${escapeHtml(ts)}${e.method}${escapeHtml(e.path)}${e.status}${e.durationMs}ms`; + return `${escapeHtml(ts)}${escapeHtml(e.method)}${escapeHtml(e.path)}${Number(e.status)}${Number(e.durationMs)}ms`; }).join(''); trafficLogEl.innerHTML = `${rows}
${t('modals.settingsWindow.table.time')}${t('modals.settingsWindow.table.method')}${t('modals.settingsWindow.table.path')}${t('modals.settingsWindow.table.status')}${t('modals.settingsWindow.table.duration')}
`; diff --git a/src/utils/dom-utils.ts b/src/utils/dom-utils.ts index 9e9291eea..ad44e1c0a 100644 --- a/src/utils/dom-utils.ts +++ b/src/utils/dom-utils.ts @@ -54,6 +54,7 @@ export function replaceChildren(el: Element, ...children: DomChild[]): void { el.appendChild(frag); } +/** @internal SECURITY: Only use with trusted, hardcoded HTML strings. Never pass user/external data. */ export function rawHtml(html: string): DocumentFragment { const tpl = document.createElement('template'); tpl.innerHTML = html; @@ -63,7 +64,7 @@ export function rawHtml(html: string): DocumentFragment { const SAFE_TAGS = new Set([ 'strong', 'em', 'b', 'i', 'br', 'p', 'ul', 'ol', 'li', 'span', 'div', 'a', ]); -const SAFE_ATTRS = new Set(['style', 'class', 'href', 'target', 'rel']); +const SAFE_ATTRS = new Set(['class', 'href', 'target', 'rel']); /** Like rawHtml() but strips tags and attributes not in the allowlist. */ export function safeHtml(html: string): DocumentFragment { diff --git a/vercel.json b/vercel.json index 684a62b1e..15187a92c 100644 --- a/vercel.json +++ b/vercel.json @@ -5,6 +5,16 @@ { "source": "/ingest/:path*", "destination": "https://us.i.posthog.com/:path*" } ], "headers": [ + { + "source": "/(.*)", + "headers": [ + { "key": "X-Content-Type-Options", "value": "nosniff" }, + { "key": "X-Frame-Options", "value": "SAMEORIGIN" }, + { "key": "Strict-Transport-Security", "value": "max-age=31536000; includeSubDomains" }, + { "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" }, + { "key": "Permissions-Policy", "value": "camera=(), microphone=(), geolocation=()" } + ] + }, { "source": "/", "headers": [