Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/rss-reader/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ function decodeEntities(value: string): string {

return value.replace(/&(#x?[0-9a-fA-F]+|[a-zA-Z]+);/g, (match, entity: string) => {
if (entity.startsWith('#x')) {
const codePoint = Number.parseInt(entity.slice(2), 16);
const codePoint = Number.parseInt(entity.slice(2, 10), 16);
return Number.isFinite(codePoint) ? String.fromCodePoint(codePoint) : match;
Comment on lines +30 to 31

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== file outline =="
ast-grep outline examples/rss-reader/src/index.tsx --view compact || true

echo "== relevant decodeEntities code =="
rg -n "decodeEntities|String.fromCodePoint|Number.parseInt|number;hex" examples/rss-reader/src/index.tsx -C 8

echo "== read file 1-80 =="
sed -n '1,80p' examples/rss-reader/src/index.tsx

echo "== deterministic JS probe =="
node - <<'JS'
const samples = [
  ['&`#x00001F600`;', 8],
  ['&`#xFFF`;', 3],
  ['&`#xFFFFFFFF`;', 8],
  ['&`#x110000`;', 5],
  ['&`#x999999999`;', 9],
];
for (const [input, sliceParam] of samples) {
  const entity = input;
  const truncatedCodePoint = Number.parseInt(entity.slice(2, 10), 16);
  const fullHex = entity.slice(2);
  const fullCodePoint = Number.parseInt(fullHex, 16);
  let out = null;
  let err = null;
  try {
    const result = Number.isFinite(truncatedCodePoint) ? String.fromCodePoint(truncatedCodePoint) : input;
    out = result;
  } catch (e) {
    err = e.toString();
  }
  console.log(JSON.stringify({ input, sliceParam, truncated: truncatedCodePoint, fullHex, fullCodePoint, out, err }));
}
JS

Repository: Karanjot786/TermUI

Length of output: 5281


🌐 Web query:

MDN String.fromCodePoint RangeError values outside the Unicode code point range 0x0000 to 0x10FFFF

💡 Result:

The JavaScript static method String.fromCodePoint throws a RangeError if any provided argument is not a valid Unicode code point [1][2]. According to the ECMAScript specification and MDN documentation, a valid code point must be an integral number within the inclusive range of 0x0000 to 0x10FFFF [1][3][2]. A RangeError is triggered if the provided argument: - Is a non-integer (e.g., 3.14) [1][2] - Is less than 0 [1][2] - Is greater than 0x10FFFF (1114111) [1][2] - Is NaN [1][4] Values outside the 0x0000 to 0x10FFFF range are considered invalid because they fall outside the defined Unicode codespace [1][2]. While UTF-16 encoding, which JavaScript uses for strings, represents higher code points using surrogate pairs, the method itself strictly validates that the input corresponds to a valid, representable Unicode code point [2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

node - <<'JS'
function decodeEntities(value) {
  const entities = {
    amp: '&',
    lt: '<',
    gt: '>',
    quot: '"',
    apos: "'",
    nbsp: ' ',
  };
  return value.replace(/&(`#x`?[0-9a-fA-F]+|[a-zA-Z]+);/g, (match, entity) => {
    if (entity.startsWith('`#x`')) {
      const codePoint = Number.parseInt(entity.slice(2, 10), 16);
      return Number.isFinite(codePoint) ? String.fromCodePoint(codePoint) : match;
    }
    if (entity.startsWith('#')) {
      const codePoint = Number.parseInt(entity.slice(1), 10);
      return Number.isFinite(codePoint) ? String.fromCodePoint(codePoint) : match;
    }
    return entities[entity.toLowerCase()] ?? match;
  });
}

const samples = [
  '&`#x00001F600`;',
  '&`#xFFF`;',
  '&`#xFFFFFFFF`;',
  '&`#x110000`;',
  '&`#x999999999`;',
  '&`#99`;',
];

for (const input of samples) {
  let result;
  try {
    result = decodeEntities(input);
  } catch (e) {
    console.log(JSON.stringify({ input, result: null, error: e.name + ': ' + e.message }));
    continue;
  }
  console.log(JSON.stringify({ input, result }));
}
JS

Repository: Karanjot786/TermUI

Length of output: 528


Denial of Service (CWE-20): Improper Input Validation

Exploitability: Theoretical

Reject oversized hexadecimal entities instead of truncating them.

entity.slice(2, 10) keeps only the first eight digits, so &#x00001F600; decodes to U+1F60 instead of completing the entity. Values such as &#xFFFFFFFF; pass Number.isFinite, then String.fromCodePoint throws RangeError. Validate the full payload length and the Unicode range before decoding.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/rss-reader/src/index.tsx` around lines 30 - 31, Update the entity
decoding logic around codePoint to parse the complete hexadecimal payload rather
than truncating it with entity.slice(2, 10). Reject payloads exceeding the
supported Unicode hexadecimal length or values outside the valid code point
range before calling String.fromCodePoint, and return match for invalid entities
while preserving valid decoding.

}

Expand Down
2 changes: 1 addition & 1 deletion packages/ui/src/MultiSelect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export class MultiSelect extends Widget {
}

get selectedOptions(): MultiSelectOption[] {
return [...this._checked].sort().map(i => this._options[i]);
return [...this._checked].sort((a, b) => a - b).map(i => this._options[i]);
}
selectNext(): void { if (this._options.length === 0) return; let n = this._cursorIndex + 1; while (n < this._options.length && this._options[n].disabled) n++; if (n < this._options.length) { this._cursorIndex = n; this.markDirty(); } }
selectPrev(): void { if (this._options.length === 0) return; let n = this._cursorIndex - 1; while (n >= 0 && this._options[n].disabled) n--; if (n >= 0) { this._cursorIndex = n; this.markDirty(); } }
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/src/prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ async function promptSelect<T = string>(options: SelectPromptOptions<T>): Promis
return;
}
const n = parseInt(trimmed, 10);
if (!isNaN(n) && n >= 1 && n <= choices.length) {
if (!Number.isNaN(n) && n >= 1 && n <= choices.length) {
rl.close();
resolve(choices[n - 1].value);
return;
Expand Down
Loading