Skip to content
Merged
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
4 changes: 2 additions & 2 deletions browser_tests/fixtures/VueNodeHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,8 @@ export class VueNodeHelpers {
getInputNumberControls(widget: Locator) {
return {
input: widget.locator('input'),
incrementButton: widget.locator('button').first(),
decrementButton: widget.locator('button').nth(1)
decrementButton: widget.getByTestId('decrement'),
incrementButton: widget.getByTestId('increment')
}
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 15 additions & 0 deletions src/lib/litegraph/src/utils/widget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,18 @@ import type { IWidgetOptions } from '@/lib/litegraph/src/types/widgets'
export function getWidgetStep(options: IWidgetOptions<unknown>): number {
return options.step2 || (options.step || 10) * 0.1
}

export function evaluateInput(input: string): number | undefined {
// Check if v is a valid equation or a number
if (/^[\d\s.()*+/-]+$/.test(input)) {
// Solve the equation if possible
try {
input = eval(input)
Copy link
Contributor

Choose a reason for hiding this comment

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

Have we explored any of the libraries that do this? Maybe this one?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We did some testing with expr-eval, but decided to move that to a future PR.

Math.js looks like a btter option.

Copy link
Contributor

Choose a reason for hiding this comment

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

@coderabbitai Open an issue to replace the evals with a library, possibly Math.js

Copy link
Contributor

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Create a GitHub issue to track replacing eval with a math library

gh issue create \
  --repo Comfy-Org/ComfyUI_frontend \
  --title "Replace eval() with math expression library (e.g., Math.js)" \
  --body "## Description

The current implementation of mathematical expression evaluation in number widgets uses \`eval()\` to parse and evaluate user input expressions. This should be replaced with a dedicated math expression library for better security and functionality.

## Context

- Current implementation: \`evaluateInput()\` function in \`src/lib/litegraph/src/utils/widget.ts\`
- Previous testing was done with [expr-eval](https://github.com/silentmatt/expr-eval)
- [Math.js](https://mathjs.org/docs/reference/functions/evaluate.html) is suggested as a potentially better option

## Related

- PR: https://github.com/Comfy-Org/ComfyUI_frontend/pull/7759
- Comment: https://github.com/Comfy-Org/ComfyUI_frontend/pull/7759#discussion_r2687812989
- Requested by: @DrJKL" \
  --assignee DrJKL

Length of output: 57


@DrJKL I've created issue #8032 to track replacing the eval() usage with a math expression library like Math.js. The issue has been assigned to you and includes references back to this PR and comment thread.


🧬 Code Graph Analysis Results

src/lib/litegraph/src/utils/widget.ts

Filename: src/lib/litegraph/src/utils/widget.ts Lines: 1-25

import type { IWidgetOptions } from '@/lib/litegraph/src/types/widgets'

/**

  • The step value for numeric widgets.
  • Use {@link IWidgetOptions.step2} if available, otherwise fallback to
  • {@link IWidgetOptions.step} which is scaled up by 10x in the legacy frontend logic.
    */
    export function getWidgetStep(options: IWidgetOptions): number {
    return options.step2 || (options.step || 10) * 0.1
    }

export function evaluateInput(input: string): number | undefined {
// Check if v is a valid equation or a number
if (/^[\d\s.()*+/-]+$/.test(input)) {
// Solve the equation if possible
try {
input = eval(input)
} catch {
// Ignore eval errors
}
}
const newValue = Number(input)
if (isNaN(newValue)) return undefined
return newValue
}

Filename: src/extensions/core/widgetInputs.ts Lines: 355-368

#isValidConnection(input: INodeInputSlot, forceUpdate?: boolean) {
// Only allow connections where the configs match
const output = this.outputs?.[0]
const config2 = (input.widget?.[GET_CONFIG] as () => InputSpec)?.()
if (!config2) return false

return !!mergeIfValid.call(
  this,
  output,
  config2,
  forceUpdate,
  this.recreateWidget
)

}


🧠 Learnings used
Learnt from: CR
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: src/lib/litegraph/CLAUDE.md:0-0
Timestamp: 2025-11-24T19:47:56.371Z
Learning: Applies to src/lib/litegraph/**/*.{js,ts,jsx,tsx} : Do not replace `&&=` or `||=` with `=` when there is no reason to do so. If you do find a reason to remove either `&&=` or `||=`, leave a comment explaining why the removal occurred

} catch {
// Ignore eval errors
}
}
const newValue = Number(input)
if (isNaN(newValue)) return undefined
return newValue
}
Comment on lines +12 to +25
Copy link
Contributor

@coderabbitai coderabbitai bot Jan 13, 2026

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider documenting the security rationale or using a safer alternative to eval.

The regex allowlist on line 14 restricts input to digits, whitespace, and arithmetic operators, which mitigates common injection vectors. However, eval remains a security-sensitive pattern that static analysis tools will continue to flag.

Since this centralizes existing behavior from NumberWidget, the risk profile is unchanged. Consider either:

  1. Adding a comment explaining why the regex makes this safe for arithmetic expressions
  2. Long-term: replacing eval with a simple expression parser (e.g., Function constructor with the same regex guard, or a lightweight math expression library)

Also, minor nit: the comment on line 13 references v but the parameter is named input.

📝 Suggested comment addition
 export function evaluateInput(input: string): number | undefined {
-  // Check if v is a valid equation or a number
+  // SECURITY: Regex allowlist restricts input to digits, whitespace, decimal points,
+  // parentheses, and arithmetic operators (+-*/). This prevents variable access,
+  // function calls, and other code execution vectors.
   if (/^[\d\s.()*+/-]+$/.test(input)) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function evaluateInput(input: string): number | undefined {
// Check if v is a valid equation or a number
if (/^[\d\s.()*+/-]+$/.test(input)) {
// Solve the equation if possible
try {
input = eval(input)
} catch {
// Ignore eval errors
}
}
const newValue = Number(input)
if (isNaN(newValue)) return undefined
return newValue
}
export function evaluateInput(input: string): number | undefined {
// SECURITY: Regex allowlist restricts input to digits, whitespace, decimal points,
// parentheses, and arithmetic operators (+-*/). This prevents variable access,
// function calls, and other code execution vectors.
if (/^[\d\s.()*+/-]+$/.test(input)) {
// Solve the equation if possible
try {
input = eval(input)
} catch {
// Ignore eval errors
}
}
const newValue = Number(input)
if (isNaN(newValue)) return undefined
return newValue
}
🧰 Tools
🪛 Biome (2.1.2)

[error] 17-17: eval() exposes to security risks and performance issues.

See the MDN web docs for more details.
Refactor the code so that it doesn't need to call eval().

(lint/security/noGlobalEval)

🤖 Prompt for AI Agents
In @src/lib/litegraph/src/utils/widget.ts around lines 12 - 25, In
evaluateInput, update the inline documentation to explain the security rationale
for using eval: state that the allowlist regex /^[\d\s.()*+/-]+$/ restricts
input to digits, whitespace and arithmetic operators (preventing
identifier/property access and injection) and therefore is intentionally used
here to evaluate arithmetic expressions; also fix the misleading comment that
refers to "v" to use the actual parameter name "input". Additionally, add a TODO
note on the evaluateInput function referencing NumberWidget to plan replacing
eval with a safer evaluator (e.g., a small math expression parser or vetted
library) as a long‑term improvement.

Copy link
Contributor

Choose a reason for hiding this comment

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

See the issue opened related to this.

Copy link
Contributor

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for recently opened issues that might be related to this PR or eval security
gh issue list --repo Comfy-Org/ComfyUI_frontend --limit 20 --state open --json number,title,url,createdAt,author | jq -r '.[] | "\(.number): \(.title) (created: \(.createdAt), author: \(.author.login))\n  URL: \(.url)"'

# Also search for issues mentioning eval or this PR number
gh issue list --repo Comfy-Org/ComfyUI_frontend --search "eval" --limit 10 --state open --json number,title,url,body | jq -r '.[] | "\(.number): \(.title)\n  URL: \(.url)\n  Body preview: \(.body[:200])"'

# Search for issues mentioning PR 7759
gh issue list --repo Comfy-Org/ComfyUI_frontend --search "7759" --limit 10 --state open --json number,title,url,body | jq -r '.[] | "\(.number): \(.title)\n  URL: \(.url)\n  Body preview: \(.body[:200])"'

Length of output: 4428


@DrJKL Thanks for opening #8032 to track the eval replacement! That's the right approach—this PR centralizes the existing behavior without changing the security profile, and the issue ensures we can tackle the safer math expression parser as a follow-up improvement.


🧬 Code Graph Analysis Results

src/extensions/core/widgetInputs.ts:355-368

#isValidConnection(input: INodeInputSlot, forceUpdate?: boolean) {
    // Only allow connections where the configs match
    const output = this.outputs?.[0]
    const config2 = (input.widget?.[GET_CONFIG] as () => InputSpec)?.()
    if (!config2) return false

    return !!mergeIfValid.call(
      this,
      output,
      config2,
      forceUpdate,
      this.recreateWidget
    )
  }

🧠 Learnings used
Learnt from: CR
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: src/lib/litegraph/CLAUDE.md:0-0
Timestamp: 2025-11-24T19:47:56.371Z
Learning: Applies to src/lib/litegraph/**/*.{js,ts,jsx,tsx} : Do not replace `&&=` or `||=` with `=` when there is no reason to do so. If you do find a reason to remove either `&&=` or `||=`, leave a comment explaining why the removal occurred

17 changes: 3 additions & 14 deletions src/lib/litegraph/src/widgets/NumberWidget.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { INumericWidget } from '@/lib/litegraph/src/types/widgets'
import { getWidgetStep } from '@/lib/litegraph/src/utils/widget'
import { evaluateInput, getWidgetStep } from '@/lib/litegraph/src/utils/widget'

import { BaseSteppedWidget } from './BaseSteppedWidget'
import type { WidgetEventOptions } from './BaseWidget'
Expand Down Expand Up @@ -68,19 +68,8 @@ export class NumberWidget
'Value',
this.value,
(v: string) => {
// Check if v is a valid equation or a number
if (/^[\d\s()*+/-]+|\d+\.\d+$/.test(v)) {
// Solve the equation if possible
try {
v = eval(v)
} catch {
// Ignore eval errors
}
}
const newValue = Number(v)
if (!isNaN(newValue)) {
this.setValue(newValue, { e, node, canvas })
}
const parsed = evaluateInput(v)
if (parsed !== undefined) this.setValue(parsed, { e, node, canvas })
},
e
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { mount } from '@vue/test-utils'
import PrimeVue from 'primevue/config'
import InputNumber from 'primevue/inputnumber'
import { describe, expect, it } from 'vitest'
import { mount } from '@vue/test-utils'
import { createI18n } from 'vue-i18n'

import type { SimplifiedWidget } from '@/types/simplifiedWidget'

import WidgetInputNumberInput from './WidgetInputNumberInput.vue'

const i18n = createI18n({
legacy: false,
locale: 'en'
})

function createMockWidget(
value: number = 0,
type: 'int' | 'float' = 'int',
Expand All @@ -24,10 +28,7 @@ function createMockWidget(

function mountComponent(widget: SimplifiedWidget<number>, modelValue: number) {
return mount(WidgetInputNumberInput, {
global: {
plugins: [PrimeVue],
components: { InputNumber }
},
global: { plugins: [i18n] },
props: {
widget,
modelValue
Expand All @@ -36,7 +37,7 @@ function mountComponent(widget: SimplifiedWidget<number>, modelValue: number) {
}

function getNumberInput(wrapper: ReturnType<typeof mount>) {
const input = wrapper.get<HTMLInputElement>('input[inputmode="numeric"]')
const input = wrapper.get<HTMLInputElement>('input[inputmode="decimal"]')
return input.element
}

Expand All @@ -53,7 +54,7 @@ describe('WidgetInputNumberInput Value Binding', () => {
const widget = createMockWidget(10, 'int')
const wrapper = mountComponent(widget, 10)

const inputNumber = wrapper.findComponent(InputNumber)
const inputNumber = wrapper
await inputNumber.vm.$emit('update:modelValue', 20)

const emitted = wrapper.emitted('update:modelValue')
Expand All @@ -78,75 +79,6 @@ describe('WidgetInputNumberInput Value Binding', () => {
})
})

describe('WidgetInputNumberInput Component Rendering', () => {
it('renders InputNumber component with show-buttons', () => {
const widget = createMockWidget(5, 'int')
const wrapper = mountComponent(widget, 5)

const inputNumber = wrapper.findComponent(InputNumber)
expect(inputNumber.exists()).toBe(true)
expect(inputNumber.props('showButtons')).toBe(true)
})

it('sets button layout to horizontal', () => {
const widget = createMockWidget(5, 'int')
const wrapper = mountComponent(widget, 5)

const inputNumber = wrapper.findComponent(InputNumber)
expect(inputNumber.props('buttonLayout')).toBe('horizontal')
})

it('sets size to small', () => {
const widget = createMockWidget(5, 'int')
const wrapper = mountComponent(widget, 5)

const inputNumber = wrapper.findComponent(InputNumber)
expect(inputNumber.props('size')).toBe('small')
})
})

describe('WidgetInputNumberInput Step Value', () => {
it('defaults to 0 for unrestricted stepping', () => {
const widget = createMockWidget(5, 'int')
const wrapper = mountComponent(widget, 5)

const inputNumber = wrapper.findComponent(InputNumber)
expect(inputNumber.props('step')).toBe(0)
})

it('uses step2 value when provided', () => {
const widget = createMockWidget(5, 'int', { step2: 0.5 })
const wrapper = mountComponent(widget, 5)

const inputNumber = wrapper.findComponent(InputNumber)
expect(inputNumber.props('step')).toBe(0.5)
})

it('calculates step from precision for precision 0', () => {
const widget = createMockWidget(5, 'int', { precision: 0 })
const wrapper = mountComponent(widget, 5)

const inputNumber = wrapper.findComponent(InputNumber)
expect(inputNumber.props('step')).toBe(1)
})

it('calculates step from precision for precision 1', () => {
const widget = createMockWidget(5, 'float', { precision: 1 })
const wrapper = mountComponent(widget, 5)

const inputNumber = wrapper.findComponent(InputNumber)
expect(inputNumber.props('step')).toBe(0.1)
})

it('calculates step from precision for precision 2', () => {
const widget = createMockWidget(5, 'float', { precision: 2 })
const wrapper = mountComponent(widget, 5)

const inputNumber = wrapper.findComponent(InputNumber)
expect(inputNumber.props('step')).toBe(0.01)
})
})

describe('WidgetInputNumberInput Grouping Behavior', () => {
it('displays numbers without commas by default for int widgets', () => {
const widget = createMockWidget(1000, 'int')
Expand Down Expand Up @@ -202,33 +134,29 @@ describe('WidgetInputNumberInput Large Integer Precision Handling', () => {
const widget = createMockWidget(1000, 'int')
const wrapper = mountComponent(widget, 1000)

const inputNumber = wrapper.findComponent(InputNumber)
expect(inputNumber.props('showButtons')).toBe(true)
expect(wrapper.findAll('button').length).toBe(2)
})

it('shows buttons for values at safe integer limit', () => {
const widget = createMockWidget(SAFE_INTEGER_MAX, 'int')
const wrapper = mountComponent(widget, SAFE_INTEGER_MAX)

const inputNumber = wrapper.findComponent(InputNumber)
expect(inputNumber.props('showButtons')).toBe(true)
expect(wrapper.findAll('button').length).toBe(2)
})

it('hides buttons for unsafe large integer values', () => {
const widget = createMockWidget(UNSAFE_LARGE_INTEGER, 'int')
const wrapper = mountComponent(widget, UNSAFE_LARGE_INTEGER)

const inputNumber = wrapper.findComponent(InputNumber)
expect(inputNumber.props('showButtons')).toBe(false)
expect(wrapper.findAll('button').length).toBe(0)
})

it('hides buttons for unsafe negative integer values', () => {
const unsafeNegative = -UNSAFE_LARGE_INTEGER
const widget = createMockWidget(unsafeNegative, 'int')
const wrapper = mountComponent(widget, unsafeNegative)

const inputNumber = wrapper.findComponent(InputNumber)
expect(inputNumber.props('showButtons')).toBe(false)
expect(wrapper.findAll('button').length).toBe(0)
})

it('shows tooltip for disabled buttons due to precision limits', (context) => {
Expand All @@ -250,43 +178,19 @@ describe('WidgetInputNumberInput Large Integer Precision Handling', () => {
expect(tooltipDiv.attributes('v-tooltip')).toBeUndefined()
})

it('handles edge case of zero value', () => {
const widget = createMockWidget(0, 'int')
const wrapper = mountComponent(widget, 0)

const inputNumber = wrapper.findComponent(InputNumber)
expect(inputNumber.props('showButtons')).toBe(true)
})

it('correctly identifies safe vs unsafe integers using Number.isSafeInteger', () => {
// Test the JavaScript behavior our component relies on
expect(Number.isSafeInteger(SAFE_INTEGER_MAX)).toBe(true)
expect(Number.isSafeInteger(SAFE_INTEGER_MAX + 1)).toBe(false)
expect(Number.isSafeInteger(UNSAFE_LARGE_INTEGER)).toBe(false)
expect(Number.isSafeInteger(-SAFE_INTEGER_MAX)).toBe(true)
expect(Number.isSafeInteger(-SAFE_INTEGER_MAX - 1)).toBe(false)
})

it('handles floating point values correctly', (context) => {
context.skip('needs diagnosis')

const safeFloat = 1000.5
const widget = createMockWidget(safeFloat, 'float')
const wrapper = mountComponent(widget, safeFloat)
it('handles floating point values correctly', () => {
const widget = createMockWidget(1000.5, 'float')
const wrapper = mountComponent(widget, 1000.5)

const inputNumber = wrapper.findComponent(InputNumber)
expect(inputNumber.props('showButtons')).toBe(true)
expect(wrapper.findAll('button').length).toBe(2)
})

it('hides buttons for unsafe floating point values', (context) => {
context.skip('needs diagnosis')

it('hides buttons for unsafe floating point values', () => {
const unsafeFloat = UNSAFE_LARGE_INTEGER + 0.5
const widget = createMockWidget(unsafeFloat, 'float')
const wrapper = mountComponent(widget, unsafeFloat)

const inputNumber = wrapper.findComponent(InputNumber)
expect(inputNumber.props('showButtons')).toBe(false)
expect(wrapper.findAll('button').length).toBe(0)
})
})

Expand All @@ -295,43 +199,35 @@ describe('WidgetInputNumberInput Edge Cases for Precision Handling', () => {
const widget = createMockWidget(0, 'int')
// Mount with undefined as modelValue
const wrapper = mount(WidgetInputNumberInput, {
global: {
plugins: [PrimeVue],
components: { InputNumber }
},
global: { plugins: [i18n] },
props: {
widget,
modelValue: undefined as any
}
})

const inputNumber = wrapper.findComponent(InputNumber)
expect(inputNumber.props('showButtons')).toBe(true) // Should default to safe behavior
expect(wrapper.findAll('button').length).toBe(2)
})

it('handles NaN values gracefully', (context) => {
context.skip('needs diagnosis')
const widget = createMockWidget(NaN, 'int')
const wrapper = mountComponent(widget, NaN)

const inputNumber = wrapper.findComponent(InputNumber)
// NaN is not a safe integer, so buttons should be hidden
expect(inputNumber.props('showButtons')).toBe(false)
expect(wrapper.findAll('button').length).toBe(0)
})

it('handles Infinity values', () => {
const widget = createMockWidget(Infinity, 'int')
const wrapper = mountComponent(widget, Infinity)

const inputNumber = wrapper.findComponent(InputNumber)
expect(inputNumber.props('showButtons')).toBe(false)
expect(wrapper.findAll('button').length).toBe(0)
})

it('handles negative Infinity values', () => {
const widget = createMockWidget(-Infinity, 'int')
const wrapper = mountComponent(widget, -Infinity)

const inputNumber = wrapper.findComponent(InputNumber)
expect(inputNumber.props('showButtons')).toBe(false)
expect(wrapper.findAll('button').length).toBe(0)
})
})
Loading