From 7adc06aae525166820c76a2da9081f0875e80326 Mon Sep 17 00:00:00 2001 From: "Azat S." Date: Thu, 24 Oct 2024 01:27:13 +0300 Subject: [PATCH] feat: prevent xss attacks --- preview/elements/note.svelte | 35 ++++++++++++++++++++++-- preview/elements/table-comment.svelte | 19 +++++++++++--- preview/stores/data.ts | 38 +++++++++++++++++++++++---- src/escape_html.rs | 9 +++++++ src/escape_json_values.rs | 22 ++++++++++++++++ src/lib.rs | 2 ++ src/main.rs | 14 +++++++--- 7 files changed, 125 insertions(+), 14 deletions(-) create mode 100644 src/escape_html.rs create mode 100644 src/escape_json_values.rs diff --git a/preview/elements/note.svelte b/preview/elements/note.svelte index 272f477..eaa4187 100644 --- a/preview/elements/note.svelte +++ b/preview/elements/note.svelte @@ -3,13 +3,44 @@ export let title = '' export let content: string[] = [] + + let processLine = (line: string) => { + let parts: Array<{ bold: boolean; text: string }> = [] + + let regex = /(.*?)<\/b>/gi + + let lastIndex = 0 + let match + + while ((match = regex.exec(line)) !== null) { + if (match.index > lastIndex) { + parts.push({ text: line.slice(lastIndex, match.index), bold: false }) + } + parts.push({ text: match[1], bold: true }) + ;({ lastIndex } = regex) + } + + if (lastIndex < line.length) { + parts.push({ text: line.slice(lastIndex), bold: false }) + } + + return parts + }
{title} + {#each content as line} - - {@html line} + + {#each processLine(line) as part} + {#if part.bold} + {part.text} + {:else} + {part.text} + {/if} + {/each} + {/each}
diff --git a/preview/elements/table-comment.svelte b/preview/elements/table-comment.svelte index efc276e..4b3d88d 100644 --- a/preview/elements/table-comment.svelte +++ b/preview/elements/table-comment.svelte @@ -4,10 +4,23 @@ export let value = '' export let kind = '' - let formattedValue = value.replace(new RegExp(kind, 'i'), '$&') + let highlightedParts: Array<{ highlighted: boolean; text: string }> = [] + + $: { + let regex = new RegExp(`(${kind})`, 'i') + highlightedParts = value.split(regex).map(part => ({ + highlighted: regex.test(part), + text: part, + })) + } - - {@html formattedValue} + {#each highlightedParts as part} + {#if part.highlighted} + {part.text} + {:else} + {part.text} + {/if} + {/each} diff --git a/preview/stores/data.ts b/preview/stores/data.ts index e29a69e..44850e2 100644 --- a/preview/stores/data.ts +++ b/preview/stores/data.ts @@ -2,20 +2,48 @@ import { readable, writable } from 'svelte/store' import type { Data } from '~/typings/index.d' +let decodeHtmlEntities = (str: string): string => { + let textArea = document.createElement('textarea') + textArea.innerHTML = str + return textArea.value +} + +let traverseAndDecode = (obj: T): T => { + if (typeof obj === 'string') { + return decodeHtmlEntities(obj) as T + } + + if (Array.isArray(obj)) { + return obj.map(item => traverseAndDecode(item)) as T + } + + if (typeof obj === 'object' && obj !== null) { + let result = {} as { [K in keyof T]: T[K] } + for (let key in obj) { + if (obj.hasOwnProperty(key)) { + result[key] = traverseAndDecode(obj[key]) + } + } + return result + } + + return obj +} + export let loading = writable(true) export let data = readable>({}, set => { let fetchData = async () => { + let dataValue: Data if (import.meta.env.MODE === 'production') { // @ts-ignore - set(window.data) - loading.set(false) + dataValue = window.data } else { let dataResponse = await fetch('/data.json') - let dataJson = await dataResponse.json() - set(dataJson) - loading.set(false) + dataValue = await dataResponse.json() } + set(traverseAndDecode(dataValue)) + loading.set(false) } fetchData() diff --git a/src/escape_html.rs b/src/escape_html.rs new file mode 100644 index 0000000..5d24c61 --- /dev/null +++ b/src/escape_html.rs @@ -0,0 +1,9 @@ +pub fn escape_html(s: &str) -> String { + s.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """) + .replace("'", "'") + .replace("/", "/") + .replace("", "<\\/script>") +} diff --git a/src/escape_json_values.rs b/src/escape_json_values.rs new file mode 100644 index 0000000..f3bdc0a --- /dev/null +++ b/src/escape_json_values.rs @@ -0,0 +1,22 @@ +use crate::escape_html::escape_html; +use serde_json::Value; + +pub fn escape_json_values(json_value: &mut Value) { + match json_value { + Value::String(s) => { + let escaped = escape_html(s); + *s = escaped; + } + Value::Array(arr) => { + for item in arr { + escape_json_values(item); + } + } + Value::Object(map) => { + for (_key, value) in map { + escape_json_values(value); + } + } + _ => {} + } +} diff --git a/src/lib.rs b/src/lib.rs index 4f68663..9269220 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,8 @@ pub mod add_missing_days; pub mod blame; pub mod check_git_repository; pub mod copy_dir_recursive; +pub mod escape_html; +pub mod escape_json_values; pub mod exec; pub mod get_comments; pub mod get_current_directory; diff --git a/src/main.rs b/src/main.rs index 035dbcd..caf618e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,6 +12,7 @@ use todoctor::add_missing_days::add_missing_days; use todoctor::blame::blame; use todoctor::check_git_repository::check_git_repository; use todoctor::copy_dir_recursive::copy_dir_recursive; +use todoctor::escape_json_values::escape_json_values; use todoctor::exec::exec; use todoctor::get_comments::get_comments; use todoctor::get_current_directory::get_current_directory; @@ -357,8 +358,11 @@ async fn main() { "version": version, }); - let json_string: String = serde_json::to_string(&json_data) - .expect("Error: Could not serialize data"); + let mut escaped_json_data = json_data.clone(); + escape_json_values(&mut escaped_json_data); + + let escaped_json_string = serde_json::to_string(&escaped_json_data) + .expect("Error: Could not serializing JSON"); let dist_path: PathBuf = get_dist_path().expect("Error: Could not get current dist path."); @@ -374,8 +378,10 @@ async fn main() { .expect("Error reading index.html"); if let Some(pos) = index_content.find("") { - let script_tag: String = - format!("", json_string); + let script_tag: String = format!( + "", + escaped_json_string + ); index_content.insert_str(pos, &script_tag); fs::write(&index_path, index_content)