A calm place to write long-form, and publish it to the open social web. skypress.blog/
0

Configure Feed

Select the types of activity you want to include in your feed.

Embeds: scheme-guard card hrefs/srcs against javascript: URLs

+56 -5
+41
src/lib/embeds/card.test.ts
··· 59 59 expect( html ).not.toContain( '<iframe' ); 60 60 } ); 61 61 } ); 62 + 63 + describe( 'renderEmbedCard — URL scheme guard', () => { 64 + it( 'neutralizes a javascript: viewUrl on an atproto card', () => { 65 + const html = renderEmbedCard( { 66 + kind: 'atproto', 67 + authorName: 'Jeremy', 68 + handle: 'jeremy.herve.bzh', 69 + text: 'hi', 70 + images: [], 71 + viewUrl: 'javascript:alert(1)', 72 + } ); 73 + expect( html ).not.toContain( 'href="javascript:alert(1)"' ); 74 + expect( html ).not.toContain( 'javascript:' ); 75 + expect( html ).toContain( 'href=""' ); 76 + } ); 77 + 78 + it( 'neutralizes a javascript: url in the video fallback href', () => { 79 + const html = renderEmbedCard( { 80 + kind: 'youtube', 81 + id: 'dQw4w9WgXcQ', 82 + title: 'x', 83 + thumbnail: 'https://i.ytimg.com/vi/dQw4w9WgXcQ/hqdefault.jpg', 84 + url: 'javascript:alert(1)', 85 + } ); 86 + expect( html ).not.toContain( 'href="javascript:alert(1)"' ); 87 + expect( html ).not.toContain( 'javascript:' ); 88 + expect( html ).toContain( 'href=""' ); 89 + } ); 90 + 91 + it( 'leaves a valid https url untouched in the href', () => { 92 + const html = renderEmbedCard( { 93 + kind: 'atproto', 94 + authorName: 'Jeremy', 95 + handle: 'jeremy.herve.bzh', 96 + text: 'hi', 97 + images: [], 98 + viewUrl: 'https://mu.social/profile/did:plc:x/post/abc', 99 + } ); 100 + expect( html ).toContain( 'href="https://mu.social/profile/did:plc:x/post/abc"' ); 101 + } ); 102 + } );
+15 -5
src/lib/embeds/card.ts
··· 33 33 34 34 export type EmbedData = AtprotoCardData | VideoCardData; 35 35 36 + /** Allow only http(s) URLs in href/src; anything else (javascript:, data:, …) → ''. */ 37 + function safeHttpUrl( value: string ): string { 38 + try { 39 + const protocol = new URL( value ).protocol; 40 + return protocol === 'https:' || protocol === 'http:' ? value : ''; 41 + } catch { 42 + return ''; 43 + } 44 + } 45 + 36 46 export function escapeHtml( value: string ): string { 37 47 return value 38 48 .replace( /&/g, '&amp;' ) ··· 44 54 45 55 function atprotoCard( d: AtprotoCardData ): string { 46 56 const avatar = d.avatar 47 - ? `<img class="skypress-embed__avatar" src="${ escapeHtml( d.avatar ) }" alt=""/>` 57 + ? `<img class="skypress-embed__avatar" src="${ escapeHtml( safeHttpUrl( d.avatar ) ) }" alt=""/>` 48 58 : ''; 49 59 const images = d.images 50 60 .map( 51 61 ( img ) => 52 - `<img class="skypress-embed__image" src="${ escapeHtml( img.src ) }" alt="${ escapeHtml( img.alt ) }"/>` 62 + `<img class="skypress-embed__image" src="${ escapeHtml( safeHttpUrl( img.src ) ) }" alt="${ escapeHtml( img.alt ) }"/>` 53 63 ) 54 64 .join( '' ); 55 65 const text = escapeHtml( d.text ).replace( /\n/g, '<br/>' ); 56 66 return ( 57 67 `<figure class="wp-block-embed skypress-embed skypress-embed--atproto">` + 58 - `<a class="skypress-embed__link" href="${ escapeHtml( d.viewUrl ) }">` + 68 + `<a class="skypress-embed__link" href="${ escapeHtml( safeHttpUrl( d.viewUrl ) ) }">` + 59 69 `<span class="skypress-embed__head">${ avatar }` + 60 70 `<span class="skypress-embed__author">${ escapeHtml( d.authorName ) }</span>` + 61 71 `<span class="skypress-embed__handle">@${ escapeHtml( d.handle ) }</span></span>` + ··· 70 80 return ( 71 81 `<figure class="wp-block-embed skypress-embed skypress-embed--video">` + 72 82 `<button type="button" class="skypress-embed__play" data-embed-provider="${ escapeHtml( d.kind ) }" data-embed-id="${ escapeHtml( d.id ) }">` + 73 - `<img class="skypress-embed__thumb" src="${ escapeHtml( d.thumbnail ) }" alt=""/>` + 83 + `<img class="skypress-embed__thumb" src="${ escapeHtml( safeHttpUrl( d.thumbnail ) ) }" alt=""/>` + 74 84 `<span class="skypress-embed__playicon" aria-hidden="true">▶</span>` + 75 85 `<span class="skypress-embed__title">${ escapeHtml( d.title ) }</span>` + 76 86 `</button>` + 77 - `<a class="skypress-embed__fallback" href="${ escapeHtml( d.url ) }">Watch on ${ d.kind === 'youtube' ? 'YouTube' : 'Vimeo' }</a>` + 87 + `<a class="skypress-embed__fallback" href="${ escapeHtml( safeHttpUrl( d.url ) ) }">Watch on ${ d.kind === 'youtube' ? 'YouTube' : 'Vimeo' }</a>` + 78 88 `</figure>` 79 89 ); 80 90 }