diff --git a/.changeset/rare-crews-collect.md b/.changeset/rare-crews-collect.md new file mode 100644 index 000000000000..a4ffe09324f0 --- /dev/null +++ b/.changeset/rare-crews-collect.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: warn on bidirectional control characters diff --git a/documentation/docs/98-reference/.generated/compile-warnings.md b/documentation/docs/98-reference/.generated/compile-warnings.md index 0e94cbadb2e8..7069f9020674 100644 --- a/documentation/docs/98-reference/.generated/compile-warnings.md +++ b/documentation/docs/98-reference/.generated/compile-warnings.md @@ -586,6 +586,14 @@ Attributes should not contain ':' characters to prevent ambiguity with Svelte di Quoted attributes on components and custom elements will be stringified in a future version of Svelte. If this isn't what you want, remove the quotes ``` +### bidirectional_control_characters + +``` +A bidirectional control character was detected in your code. These characters can be used to alter the visual direction of your code and could have unintended consequences +``` + +Bidirectional control characters can alter the direction in which text appears to be in. For example, via control characters, you can make `defabc` look like `abcdef`. As a result, if you were to unknowingly copy and paste some code that has these control characters, they may alter the behavior of your code in ways you did not intend. See [trojansource.codes](https://trojansource.codes/) for more information. + ### bind_invalid_each_rest ``` diff --git a/packages/svelte/messages/compile-warnings/misc.md b/packages/svelte/messages/compile-warnings/misc.md index 3b977db1be57..29343dd28a33 100644 --- a/packages/svelte/messages/compile-warnings/misc.md +++ b/packages/svelte/messages/compile-warnings/misc.md @@ -1,3 +1,9 @@ +## bidirectional_control_characters + +> A bidirectional control character was detected in your code. These characters can be used to alter the visual direction of your code and could have unintended consequences + +Bidirectional control characters can alter the direction in which text appears to be in. For example, via control characters, you can make `defabc` look like `abcdef`. As a result, if you were to unknowingly copy and paste some code that has these control characters, they may alter the behavior of your code in ways you did not intend. See [trojansource.codes](https://trojansource.codes/) for more information. + ## legacy_code > `%code%` is no longer valid — please use `%suggestion%` instead diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 2e36a896493f..766b317d06e2 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -43,6 +43,7 @@ import { ImportDeclaration } from './visitors/ImportDeclaration.js'; import { KeyBlock } from './visitors/KeyBlock.js'; import { LabeledStatement } from './visitors/LabeledStatement.js'; import { LetDirective } from './visitors/LetDirective.js'; +import { Literal } from './visitors/Literal.js'; import { MemberExpression } from './visitors/MemberExpression.js'; import { NewExpression } from './visitors/NewExpression.js'; import { OnDirective } from './visitors/OnDirective.js'; @@ -63,6 +64,7 @@ import { SvelteSelf } from './visitors/SvelteSelf.js'; import { SvelteWindow } from './visitors/SvelteWindow.js'; import { SvelteBoundary } from './visitors/SvelteBoundary.js'; import { TaggedTemplateExpression } from './visitors/TaggedTemplateExpression.js'; +import { TemplateElement } from './visitors/TemplateElement.js'; import { Text } from './visitors/Text.js'; import { TitleElement } from './visitors/TitleElement.js'; import { TransitionDirective } from './visitors/TransitionDirective.js'; @@ -156,6 +158,7 @@ const visitors = { KeyBlock, LabeledStatement, LetDirective, + Literal, MemberExpression, NewExpression, OnDirective, @@ -176,6 +179,7 @@ const visitors = { SvelteWindow, SvelteBoundary, TaggedTemplateExpression, + TemplateElement, Text, TransitionDirective, TitleElement, diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Literal.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Literal.js new file mode 100644 index 000000000000..58684ba71ca0 --- /dev/null +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Literal.js @@ -0,0 +1,14 @@ +/** @import { Literal } from 'estree' */ +import * as w from '../../../warnings.js'; +import { regex_bidirectional_control_characters } from '../../patterns.js'; + +/** + * @param {Literal} node + */ +export function Literal(node) { + if (typeof node.value === 'string') { + if (regex_bidirectional_control_characters.test(node.value)) { + w.bidirectional_control_characters(node); + } + } +} diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/TemplateElement.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/TemplateElement.js new file mode 100644 index 000000000000..978042bbc589 --- /dev/null +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/TemplateElement.js @@ -0,0 +1,12 @@ +/** @import { TemplateElement } from 'estree' */ +import * as w from '../../../warnings.js'; +import { regex_bidirectional_control_characters } from '../../patterns.js'; + +/** + * @param {TemplateElement} node + */ +export function TemplateElement(node) { + if (regex_bidirectional_control_characters.test(node.value.cooked ?? '')) { + w.bidirectional_control_characters(node); + } +} diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Text.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Text.js index 363a111b7dc6..a03421e8dd26 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Text.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Text.js @@ -1,20 +1,52 @@ /** @import { AST } from '#compiler' */ /** @import { Context } from '../types' */ import { is_tag_valid_with_parent } from '../../../../html-tree-validation.js'; -import { regex_not_whitespace } from '../../patterns.js'; +import { regex_bidirectional_control_characters, regex_not_whitespace } from '../../patterns.js'; import * as e from '../../../errors.js'; +import * as w from '../../../warnings.js'; +import { extract_svelte_ignore } from '../../../utils/extract_svelte_ignore.js'; /** * @param {AST.Text} node * @param {Context} context */ export function Text(node, context) { - const in_template = context.path.at(-1)?.type === 'Fragment'; + const parent = /** @type {AST.SvelteNode} */ (context.path.at(-1)); - if (in_template && context.state.parent_element && regex_not_whitespace.test(node.data)) { + if ( + parent.type === 'Fragment' && + context.state.parent_element && + regex_not_whitespace.test(node.data) + ) { const message = is_tag_valid_with_parent('#text', context.state.parent_element); if (message) { e.node_invalid_placement(node, message); } } + + regex_bidirectional_control_characters.lastIndex = 0; + for (const match of node.data.matchAll(regex_bidirectional_control_characters)) { + let is_ignored = false; + + // if we have a svelte-ignore comment earlier in the text, bail + // (otherwise we can only use svelte-ignore on parent elements/blocks) + if (parent.type === 'Fragment') { + for (const child of parent.nodes) { + if (child === node) break; + + if (child.type === 'Comment') { + is_ignored ||= extract_svelte_ignore( + child.start + 4, + child.data, + context.state.analysis.runes + ).includes('bidirectional_control_characters'); + } + } + } + + if (!is_ignored) { + let start = match.index + node.start; + w.bidirectional_control_characters({ start, end: start + match[0].length }); + } + } } diff --git a/packages/svelte/src/compiler/phases/patterns.js b/packages/svelte/src/compiler/phases/patterns.js index bda299de9e18..2bee717131c7 100644 --- a/packages/svelte/src/compiler/phases/patterns.js +++ b/packages/svelte/src/compiler/phases/patterns.js @@ -21,3 +21,5 @@ export const regex_invalid_identifier_chars = /(^[^a-zA-Z_$]|[^a-zA-Z0-9_$])/g; export const regex_starts_with_vowel = /^[aeiou]/; export const regex_heading_tags = /^h[1-6]$/; export const regex_illegal_attribute_character = /(^[0-9-.])|[\^$@%?!|()[\]{}^*+~;]/; +export const regex_bidirectional_control_characters = + /[\u202a\u202b\u202c\u202d\u202e\u2066\u2067\u2068\u2069]+/g; diff --git a/packages/svelte/src/compiler/warnings.js b/packages/svelte/src/compiler/warnings.js index e6fc8caba54f..c281433213e8 100644 --- a/packages/svelte/src/compiler/warnings.js +++ b/packages/svelte/src/compiler/warnings.js @@ -86,6 +86,7 @@ export const codes = [ 'a11y_role_supports_aria_props_implicit', 'a11y_unknown_aria_attribute', 'a11y_unknown_role', + 'bidirectional_control_characters', 'legacy_code', 'unknown_code', 'options_deprecated_accessors', @@ -506,6 +507,14 @@ export function a11y_unknown_role(node, role, suggestion) { w(node, 'a11y_unknown_role', `${suggestion ? `Unknown role '${role}'. Did you mean '${suggestion}'?` : `Unknown role '${role}'`}\nhttps://svelte.dev/e/a11y_unknown_role`); } +/** + * A bidirectional control character was detected in your code. These characters can be used to alter the visual direction of your code and could have unintended consequences + * @param {null | NodeLike} node + */ +export function bidirectional_control_characters(node) { + w(node, 'bidirectional_control_characters', `A bidirectional control character was detected in your code. These characters can be used to alter the visual direction of your code and could have unintended consequences\nhttps://svelte.dev/e/bidirectional_control_characters`); +} + /** * `%code%` is no longer valid — please use `%suggestion%` instead * @param {null | NodeLike} node diff --git a/packages/svelte/tests/validator/samples/bidirectional-control-characters/input.svelte b/packages/svelte/tests/validator/samples/bidirectional-control-characters/input.svelte new file mode 100644 index 000000000000..21587e5f4fe8 --- /dev/null +++ b/packages/svelte/tests/validator/samples/bidirectional-control-characters/input.svelte @@ -0,0 +1,8 @@ + +defabc +