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

View file

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

View file

@ -19,7 +19,7 @@ import {
restoreScrollPosition,
saveScrollPosition,
} from "@utils/browser";
import { debounce, randomStr } from "@utils/helpers";
import { debounce, randomStr, unescapeHTML } from "@utils/helpers";
import { isImage } from "@utils/media";
import { RouteDataResponse } from "@utils/types";
import autosize from "autosize";
@ -325,8 +325,9 @@ export class Post extends Component<any, PostState> {
get documentTitle(): string {
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"
? `${this.state.postRes.data.post_view.post.name} - ${siteName}`
? `${postTitle} - ${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 randomStr from "./random-str";
import removeAuthParam from "./remove-auth-param";
import returnStringFromString from "./return-str";
import sleep from "./sleep";
import validEmail from "./valid-email";
import validInstanceTLD from "./valid-instance-tld";
import validTitle from "./valid-title";
import validURL from "./valid-url";
import { escapeHTML, unescapeHTML } from "./html-entities";
export {
capitalizeFirstLetter,
debounce,
editListImmutable,
escapeHTML,
formatPastDate,
futureDaysToUnixTime,
getIdFromString,
@ -45,7 +48,9 @@ export {
poll,
randomStr,
removeAuthParam,
returnStringFromString,
sleep,
unescapeHTML,
validEmail,
validInstanceTLD,
validTitle,