Skip to content
Open
Show file tree
Hide file tree
Changes from 17 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
3 changes: 3 additions & 0 deletions src/packages/button/button.taro.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export const Button = React.forwardRef<HTMLButtonElement, Partial<ButtonProps>>(
...rest
} = { ...defaultProps, ...props }

const role = 'button'
const getStyle = useMemo(() => {
const style: CSSProperties = {}
if (color) {
Expand Down Expand Up @@ -161,6 +162,8 @@ export const Button = React.forwardRef<HTMLButtonElement, Partial<ButtonProps>>(
className={buttonClassNames}
style={{ ...getStyle, ...style }}
onClick={(e) => handleClick(e as any)}
ariaRole={role}
ariaDisabled={disabled}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

加一个 button.tsx 下的多语言

>
<View className="nut-button-wrap">
{loading && <Loading className="nut-icon-loading" />}
Expand Down
3 changes: 3 additions & 0 deletions src/packages/checkbox/checkbox.taro.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,9 @@ export const Checkbox: FC<
)}
{...rest}
onClick={handleClick}
ariaRole="checkbox"
tabIndex={innerDisabled ? -1 : 0}
ariaChecked={innerIndeterminate ? 'mixed' : innerChecked}
>
{renderCheckboxItem()}
</View>
Expand Down
3 changes: 3 additions & 0 deletions src/packages/checkbox/checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,9 @@ export const Checkbox: FunctionComponent<
)}
{...rest}
onClick={handleClick}
role="checkbox"
tabIndex={innerDisabled ? -1 : 0}
aria-checked={innerIndeterminate ? 'mixed' : innerChecked}
>
{renderCheckboxItem()}
</div>
Expand Down
20 changes: 20 additions & 0 deletions src/packages/countdown/countdown.taro.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const defaultProps = {
autoStart: true,
time: 0,
destroy: false,
ariaLabel: '倒计时',
} as TaroCountDownProps

const InternalCountDown: ForwardRefRenderFunction<
Expand All @@ -48,6 +49,7 @@ const InternalCountDown: ForwardRefRenderFunction<
onRestart,
onUpdate,
children,
ariaLabel,
...rest
} = { ...defaultProps, ...props }
const classPrefix = 'nut-countdown'
Expand All @@ -64,6 +66,11 @@ const InternalCountDown: ForwardRefRenderFunction<
diffTime: 0, // 设置了 startTime 时,与 date.now() 的差异
})

const [role, setRole] = useState('')
// ARIA alert提示内容
const [alertContent, setAlertContent] = useState('')
const alertTimerRef = useRef<ReturnType<typeof setTimeout>>()

// 时间戳转换 或 获取当前时间的时间戳
const getTimeStamp = (timeStr?: string | number) => {
if (!timeStr) return Date.now()
Expand Down Expand Up @@ -102,6 +109,12 @@ const InternalCountDown: ForwardRefRenderFunction<
stateRef.current.counting = false
pause()
onEnd && onEnd()
setRole('alert')
setAlertContent(`${ariaLabel}倒计时结束`)
alertTimerRef.current = setTimeout(() => {
setRole('')
setAlertContent('')
}, 3000)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

if (remainTime > 0) {
Expand Down Expand Up @@ -257,6 +270,9 @@ const InternalCountDown: ForwardRefRenderFunction<

const componentWillUnmount = () => {
destroy && cancelAnimationFrame(stateRef.current.timer)
if (alertTimerRef.current) {
clearTimeout(alertTimerRef.current)
}
}

const getUnit = (unit: string) => {
Expand Down Expand Up @@ -327,9 +343,13 @@ const InternalCountDown: ForwardRefRenderFunction<
<View
className={`${classPrefix} ${className}`}
style={{ ...style }}
ariaLabel={ariaLabel}
{...rest}
>
{renderTaroTime()}
<View role={role} style={{ display: 'none' }}>
{alertContent}
</View>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
</View>
)}
</>
Expand Down
19 changes: 18 additions & 1 deletion src/packages/countdown/countdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const defaultProps = {
autoStart: true,
time: 0,
destroy: false,
ariaLabel: '倒计时',
} as WebCountDownProps

const InternalCountDown: ForwardRefRenderFunction<
Expand All @@ -45,6 +46,7 @@ const InternalCountDown: ForwardRefRenderFunction<
onRestart,
onUpdate,
children,
ariaLabel,
...rest
} = { ...defaultProps, ...props }
const classPrefix = 'nut-countdown'
Expand All @@ -61,6 +63,11 @@ const InternalCountDown: ForwardRefRenderFunction<
diffTime: 0, // 设置了 startTime 时,与 date.now() 的差异
})

const [role, setRole] = useState('')
// ARIA alert提示内容
const [alertContent, setAlertContent] = useState('')
const alertTimerRef = useRef<number>()

// 时间戳转换 或 获取当前时间的时间戳
const getTimeStamp = (timeStr?: string | number) => {
if (!timeStr) return Date.now()
Expand Down Expand Up @@ -97,6 +104,12 @@ const InternalCountDown: ForwardRefRenderFunction<
stateRef.current.counting = false
pause()
onEnd && onEnd()
setRole('alert')
setAlertContent(`${ariaLabel}倒计时结束`)
alertTimerRef.current = window.setTimeout(() => {
setRole('')
setAlertContent('')
}, 3000)
}

if (remainTime > 0) {
Expand Down Expand Up @@ -270,6 +283,9 @@ const InternalCountDown: ForwardRefRenderFunction<

const componentWillUnmount = () => {
destroy && cancelAnimationFrame(stateRef.current.timer)
if (alertTimerRef.current) {
clearTimeout(alertTimerRef.current)
}
}

const renderTime = (() => {
Expand All @@ -282,9 +298,10 @@ const InternalCountDown: ForwardRefRenderFunction<
<div
className={`${classPrefix} ${className}`}
style={{ ...style }}
aria-label={ariaLabel}
{...rest}
dangerouslySetInnerHTML={{
__html: `${renderTime}`,
__html: `${renderTime}<span style="display:none" role=${role}>${alertContent}</span>`,
}}
Comment on lines 302 to 305
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

存在 XSS 安全风险,建议重构

使用 dangerouslySetInnerHTML 注入包含用户提供的 ariaLabel 的内容存在跨站脚本攻击(XSS)风险。虽然 ariaLabel 有默认值,但如果开发者传入了包含恶意脚本的字符串,这些脚本会被执行。

建议采用更安全的实现方式:

方案一:使用独立的 React 元素(推荐)

-<div
-  className={`${classPrefix} ${className}`}
-  style={{ ...style }}
-  aria-label={ariaLabel}
-  {...rest}
-  dangerouslySetInnerHTML={{
-    __html: `${renderTime}<span style="display:none" role=${role}>${alertContent}</span>`,
-  }}
-/>
+<div
+  className={`${classPrefix} ${className}`}
+  style={{ ...style }}
+  aria-label={ariaLabel}
+  {...rest}
+>
+  <span dangerouslySetInnerHTML={{ __html: renderTime }} />
+  <span style={{ display: 'none' }} role={role}>
+    {alertContent}
+  </span>
+</div>

方案二:如果必须使用 HTML,进行内容转义
使用 DOMPurify 或类似库对 ariaLabel 进行清理:

import DOMPurify from 'dompurify'
const sanitizedLabel = DOMPurify.sanitize(ariaLabel)

基于静态分析工具的提示。

🧰 Tools
🪛 ast-grep (0.39.9)

[warning] 302-302: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html

(react-unsafe-html-injection)

🤖 Prompt for AI Agents
In src/packages/countdown/countdown.tsx around lines 302 to 305, the code uses
dangerouslySetInnerHTML to inject renderTime plus an ariaLabel-derived string,
which creates an XSS risk; replace this with safe React elements: render the
rendered time as normal children and add a separate span element (visually
hidden if needed) with the appropriate role and the ariaLabel/alertContent as
plain text child so React escapes it automatically; if you absolutely must
inject HTML instead, sanitize ariaLabel first with a library like DOMPurify and
use the sanitized result in dangerouslySetInnerHTML.

/>
)}
Expand Down
6 changes: 6 additions & 0 deletions src/packages/countdown/demo.taro.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import Demo6 from './demos/taro/demo6'
import Demo7 from './demos/taro/demo7'
import Demo8 from './demos/taro/demo8'
import Demo9 from './demos/taro/demo9'
import Demo10 from './demos/taro/demo10'

const CountDownDemo = () => {
const [translated] = useTranslate({
Expand All @@ -25,6 +26,7 @@ const CountDownDemo = () => {
controlTime: '控制开始和暂停的倒计时',
customStyle: '自定义展示样式',
handleControl: '手动控制',
supportAria: '支持ARIA',
},
'zh-TW': {
basic: '基础用法',
Expand All @@ -36,6 +38,7 @@ const CountDownDemo = () => {
controlTime: '控製開始和暫停的倒計時',
customStyle: '自定義展示樣式',
handleControl: '手動控製',
supportAria: '支持ARIA',
},
'en-US': {
basic: 'Basic Usage',
Expand All @@ -47,6 +50,7 @@ const CountDownDemo = () => {
controlTime: 'Manual Control',
customStyle: 'Custom Style',
handleControl: 'Handle Control',
supportAria: 'support ARIA',
},
})

Expand Down Expand Up @@ -76,6 +80,8 @@ const CountDownDemo = () => {
<Demo8 />
<View className="h2">{translated.handleControl}</View>
<Demo9 />
<View className="h2">{translated.supportAria}</View>
<Demo10 />
</ScrollView>
</>
)
Expand Down
8 changes: 7 additions & 1 deletion src/packages/countdown/demo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import Demo6 from './demos/h5/demo6'
import Demo7 from './demos/h5/demo7'
import Demo8 from './demos/h5/demo8'
import Demo9 from './demos/h5/demo9'
import Demo10 from './demos/h5/demo10'

const CountDownDemo = () => {
const [translated] = useTranslate({
Expand All @@ -22,9 +23,10 @@ const CountDownDemo = () => {
controlTime: '控制开始和暂停的倒计时',
customStyle: '自定义展示样式',
handleControl: '手动控制',
supportAria: '支持ARIA',
},
'zh-TW': {
basic: '基础用法',
basic: '基礎用法',
remainingTime: '剩余時間用法',
format: '自定義格式',
millisecond: '毫秒級渲染',
Expand All @@ -33,6 +35,7 @@ const CountDownDemo = () => {
controlTime: '控製開始和暫停的倒計時',
customStyle: '自定義展示樣式',
handleControl: '手動控製',
supportAria: '支持ARIA',
},
'en-US': {
basic: 'Basic Usage',
Expand All @@ -44,6 +47,7 @@ const CountDownDemo = () => {
controlTime: 'Manual Control',
customStyle: 'Custom Style',
handleControl: 'Handle Control',
supportAria: 'support ARIA',
},
})

Expand All @@ -68,6 +72,8 @@ const CountDownDemo = () => {
<Demo8 />
<h2>{translated.handleControl}</h2>
<Demo9 />
<h2>{translated.supportAria}</h2>
<Demo10 />
</div>
</>
)
Expand Down
39 changes: 39 additions & 0 deletions src/packages/countdown/demos/h5/demo10.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React, { useRef } from 'react'
import { Cell, CountDown } from '@nutui/nutui-react'

const Demo1 = () => {
const stateRef = useRef({
endTime: Date.now() + 60 * 1000,
})
const onEnd = () => {
console.log('countdown: ended.')
}
return (
<>
<Cell>
<CountDown
endTime={stateRef.current.endTime}
type="primary"
onEnd={onEnd}
ariaLabel="双十一活动倒计时"
/>
</Cell>
<Cell>
<CountDown
endTime={stateRef.current.endTime}
onEnd={onEnd}
ariaLabel="双十一活动倒计时"
/>
</Cell>
<Cell>
<CountDown
endTime={stateRef.current.endTime}
type="text"
onEnd={onEnd}
ariaLabel="双十一活动倒计时"
/>
</Cell>
</>
)
}
export default Demo1
39 changes: 39 additions & 0 deletions src/packages/countdown/demos/taro/demo10.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React, { useRef } from 'react'
import { Cell, CountDown } from '@nutui/nutui-react-taro'

const Demo1 = () => {
const stateRef = useRef({
endTime: Date.now() + 60 * 1000,
})
const onEnd = () => {
console.log('countdown: ended.')
}
return (
<>
<Cell>
<CountDown
endTime={stateRef.current.endTime}
type="primary"
onEnd={onEnd}
ariaLabel="双十一活动倒计时"
/>
</Cell>
<Cell>
<CountDown
endTime={stateRef.current.endTime}
onEnd={onEnd}
ariaLabel="双十一活动倒计时"
/>
</Cell>
<Cell>
<CountDown
endTime={stateRef.current.endTime}
type="text"
onEnd={onEnd}
ariaLabel="双十一活动倒计时"
/>
</Cell>
</>
)
}
export default Demo1
5 changes: 5 additions & 0 deletions src/packages/dialog/content.taro.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export const Content: FunctionComponent<
style,
className,
onClick,
ariaRole,
ariaLabel,
} = { ...defaultContentProps, ...props }

const classPrefix = 'nut-dialog'
Expand Down Expand Up @@ -59,6 +61,9 @@ export const Content: FunctionComponent<
className={classNames(`${classPrefix}-outer`, className)}
style={style}
onClick={(e: ITouchEvent) => handleClick(e)}
ariaRole={ariaRole}
ariaLabel={ariaLabel}
tabindex={-1}
>
{close}
{header}
Expand Down
5 changes: 5 additions & 0 deletions src/packages/dialog/content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export const Content: FunctionComponent<
style,
className,
onClick,
ariaRole,
ariaLabel,
} = { ...defaultContentProps, ...props }

const classPrefix = 'nut-dialog'
Expand Down Expand Up @@ -58,6 +60,9 @@ export const Content: FunctionComponent<
className={classNames(`${classPrefix}-outer`, className)}
style={style}
onClick={handleClick}
role={ariaRole}
aria-label={ariaLabel}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

这个是否可以生效~看上去不符合react的语法~~

tabIndex={-1}
>
{close}
{header}
Expand Down
Loading