Improve handling/escaping of a few possible html entities for posts titles (&) - does not fix backend, only frontend display (#2087)

Co-authored-by: Beehaw Dev <dev@beehaw.dev>
This commit is contained in:
Security-Chief-Odo 2023-08-26 18:50:29 -05:00 committed by GitHub
parent 02bb1b84e1
commit 8977b5353a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 92 additions and 18 deletions

View file

@ -1,6 +1,7 @@
import { Component } from "inferno"; import { Component } from "inferno";
import { Post } from "lemmy-js-client"; import { Post } from "lemmy-js-client";
import * as sanitizeHtml from "sanitize-html"; import * as sanitizeHtml from "sanitize-html";
import { unescapeHTML } from "@utils/helpers";
import { relTags } from "../../config"; import { relTags } from "../../config";
import { Icon } from "../common/icon"; import { Icon } from "../common/icon";
@ -25,17 +26,21 @@ export class MetadataCard extends Component<MetadataCardProps> {
{post.name !== post.embed_title && ( {post.name !== post.embed_title && (
<> <>
<h5 className="card-title d-inline"> <h5 className="card-title d-inline">
<a className="text-body" href={post.url} rel={relTags}> <a
{post.embed_title} className="text-body"
href={unescapeHTML(post.url)}
rel={relTags}
>
{unescapeHTML(post.embed_title)}
</a> </a>
</h5> </h5>
<span className="d-inline-block ms-2 mb-2 small text-muted"> <span className="d-inline-block ms-2 mb-2 small text-muted">
<a <a
className="text-muted fst-italic" className="text-muted fst-italic"
href={post.url} href={unescapeHTML(post.url)}
rel={relTags} rel={relTags}
> >
{new URL(post.url).hostname} {new URL(unescapeHTML(post.url)).hostname}
<Icon icon="external-link" classes="ms-1" /> <Icon icon="external-link" classes="ms-1" />
</a> </a>
</span> </span>

View file

@ -5,6 +5,7 @@ import {
capitalizeFirstLetter, capitalizeFirstLetter,
futureDaysToUnixTime, futureDaysToUnixTime,
hostname, hostname,
unescapeHTML,
} from "@utils/helpers"; } from "@utils/helpers";
import { isImage, isVideo } from "@utils/media"; import { isImage, isVideo } from "@utils/media";
import { import {
@ -206,9 +207,9 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
<> <>
{this.listing()} {this.listing()}
{this.state.imageExpanded && !this.props.hideImage && this.img} {this.state.imageExpanded && !this.props.hideImage && this.img}
{this.showBody && post.url && post.embed_title && ( {this.showBody &&
<MetadataCard post={post} /> unescapeHTML(post.url) &&
)} unescapeHTML(post.embed_title) && <MetadataCard post={post} />}
{this.showBody && this.body()} {this.showBody && this.body()}
</> </>
) : ( ) : (
@ -285,8 +286,8 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
<iframe <iframe
allowFullScreen allowFullScreen
className="post-metadata-iframe" className="post-metadata-iframe"
src={post.embed_video_url} src={unescapeHTML(post.embed_video_url)}
title={post.embed_title} title={unescapeHTML(post.embed_title)}
></iframe> ></iframe>
</div> </div>
); );
@ -309,7 +310,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
get imageSrc(): string | undefined { get imageSrc(): string | undefined {
const post = this.postView.post; const post = this.postView.post;
const url = post.url; const url = unescapeHTML(post.url);
const thumbnail = post.thumbnail_url; const thumbnail = post.thumbnail_url;
if (thumbnail) { if (thumbnail) {
@ -323,7 +324,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
thumbnail() { thumbnail() {
const post = this.postView.post; const post = this.postView.post;
const url = post.url; const url = unescapeHTML(post.url);
const thumbnail = post.thumbnail_url; const thumbnail = post.thumbnail_url;
if (!this.props.hideImage && url && isImage(url) && this.imageSrc) { if (!this.props.hideImage && url && isImage(url) && this.imageSrc) {
@ -449,7 +450,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
> >
<span <span
className="d-inline" className="d-inline"
dangerouslySetInnerHTML={mdToHtmlInline(post.name)} dangerouslySetInnerHTML={mdToHtmlInline(unescapeHTML(post.name))}
/> />
</Link> </Link>
); );
@ -457,7 +458,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
postTitleLine() { postTitleLine() {
const post = this.postView.post; const post = this.postView.post;
const url = post.url; const url = unescapeHTML(post.url);
return ( return (
<> <>
@ -473,7 +474,9 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
href={url} href={url}
title={url} title={url}
rel={relTags} rel={relTags}
dangerouslySetInnerHTML={mdToHtmlInline(post.name)} dangerouslySetInnerHTML={mdToHtmlInline(
unescapeHTML(post.name),
)}
></a> ></a>
) : ( ) : (
this.postLink this.postLink
@ -486,7 +489,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
* MetadataCard/body toggle. * MetadataCard/body toggle.
*/} */}
{!this.props.showBody && {!this.props.showBody &&
((post.url && post.embed_title) || post.body) && (unescapeHTML(post.url && post.embed_title) || post.body) &&
this.showPreviewButton()} this.showPreviewButton()}
{post.removed && ( {post.removed && (
@ -548,7 +551,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
urlLine() { urlLine() {
const post = this.postView.post; const post = this.postView.post;
const url = post.url; const url = unescapeHTML(post.url);
return ( return (
<p className="small m-0"> <p className="small m-0">

View file

@ -19,7 +19,7 @@ import {
restoreScrollPosition, restoreScrollPosition,
saveScrollPosition, saveScrollPosition,
} from "@utils/browser"; } from "@utils/browser";
import { debounce, randomStr } from "@utils/helpers"; import { debounce, randomStr, unescapeHTML } from "@utils/helpers";
import { isImage } from "@utils/media"; import { isImage } from "@utils/media";
import { RouteDataResponse } from "@utils/types"; import { RouteDataResponse } from "@utils/types";
import autosize from "autosize"; import autosize from "autosize";
@ -325,8 +325,9 @@ export class Post extends Component<any, PostState> {
get documentTitle(): string { get documentTitle(): string {
const siteName = this.state.siteRes.site_view.site.name; const siteName = this.state.siteRes.site_view.site.name;
const postTitle = unescapeHTML(this.state.postRes.data.post_view.post.name);
return this.state.postRes.state === "success" return this.state.postRes.state === "success"
? `${this.state.postRes.data.post_view.post.name} - ${siteName}` ? `${postTitle} - ${siteName}`
: siteName; : siteName;
} }

View file

@ -0,0 +1,60 @@
const matchEscHtmlRx = /["'&<>]/;
const matchUnEscRx = /&(?:amp|#38|lt|#60|gt|#62|apos|#39|quot|#34);/g;
export function escapeHTML(str: string): string {
const matchEscHtml = matchEscHtmlRx.exec(str);
if (!matchEscHtml) {
return str;
}
let escape;
let html = "";
let index = 0;
let lastIndex = 0;
for (index = matchEscHtml.index; index < str.length; index++) {
switch (str.charCodeAt(index)) {
case 34: // "
escape = "&quot;";
break;
case 38: // &
escape = "&amp;";
break;
case 39: // '
escape = "&#39;";
break;
case 60: // <
escape = "&lt;";
break;
case 62: // >
escape = "&gt;";
break;
default:
continue;
}
if (lastIndex !== index) {
html += str.substring(lastIndex, index);
}
lastIndex = index + 1;
html += escape;
}
return lastIndex !== index ? html + str.substring(lastIndex, index) : html;
}
export function unescapeHTML(str: string): string {
const matchUnEsc = matchUnEscRx.exec(str);
if (!matchUnEsc) {
return str;
}
const res = str
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
.replace(/&#x3A;/g, ":")
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&amp;/g, "&");
return unescapeHTML(res);
}

View file

@ -18,16 +18,19 @@ import numToSI from "./num-to-si";
import poll from "./poll"; import poll from "./poll";
import randomStr from "./random-str"; import randomStr from "./random-str";
import removeAuthParam from "./remove-auth-param"; import removeAuthParam from "./remove-auth-param";
import returnStringFromString from "./return-str";
import sleep from "./sleep"; import sleep from "./sleep";
import validEmail from "./valid-email"; import validEmail from "./valid-email";
import validInstanceTLD from "./valid-instance-tld"; import validInstanceTLD from "./valid-instance-tld";
import validTitle from "./valid-title"; import validTitle from "./valid-title";
import validURL from "./valid-url"; import validURL from "./valid-url";
import { escapeHTML, unescapeHTML } from "./html-entities";
export { export {
capitalizeFirstLetter, capitalizeFirstLetter,
debounce, debounce,
editListImmutable, editListImmutable,
escapeHTML,
formatPastDate, formatPastDate,
futureDaysToUnixTime, futureDaysToUnixTime,
getIdFromString, getIdFromString,
@ -45,7 +48,9 @@ export {
poll, poll,
randomStr, randomStr,
removeAuthParam, removeAuthParam,
returnStringFromString,
sleep, sleep,
unescapeHTML,
validEmail, validEmail,
validInstanceTLD, validInstanceTLD,
validTitle, validTitle,