Skip to content
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
1 change: 1 addition & 0 deletions src/packages/button/button.taro.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ export const Button = React.forwardRef<HTMLButtonElement, Partial<ButtonProps>>(
className={buttonClassNames}
style={{ ...getStyle, ...style }}
onClick={(e) => handleClick(e as any)}
ariaRole="button"
>
<View className="nut-button-wrap">
{loading && <Loading className="nut-icon-loading" />}
Expand Down
2 changes: 2 additions & 0 deletions src/packages/checkbox/checkbox.taro.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,8 @@ export const Checkbox: FC<
)}
{...rest}
onClick={handleClick}
ariaRole="checkbox"
ariaChecked={innerChecked && !innerIndeterminate}
>
{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<number>()

// 时间戳转换 或 获取当前时间的时间戳
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 = window.setTimeout(() => {
setRole('')
setAlertContent('')
}, 3000)
}

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>
</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

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: '手动控制',
ariaBasic: 'ARIA基础用法',
},
'zh-TW': {
basic: '基础用法',
Expand All @@ -36,6 +38,7 @@ const CountDownDemo = () => {
controlTime: '控製開始和暫停的倒計時',
customStyle: '自定義展示樣式',
handleControl: '手動控製',
ariaBasic: 'ARIA基础用法',
},
'en-US': {
basic: 'Basic Usage',
Expand All @@ -47,6 +50,7 @@ const CountDownDemo = () => {
controlTime: 'Manual Control',
customStyle: 'Custom Style',
handleControl: 'Handle Control',
ariaBasic: 'ARIA Basic Usage',
},
})

Expand Down Expand Up @@ -76,6 +80,8 @@ const CountDownDemo = () => {
<Demo8 />
<View className="h2">{translated.handleControl}</View>
<Demo9 />
<View className="h2">{translated.ariaBasic}</View>
<Demo10 />
</ScrollView>
</>
)
Expand Down
6 changes: 6 additions & 0 deletions 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,6 +23,7 @@ const CountDownDemo = () => {
controlTime: '控制开始和暂停的倒计时',
customStyle: '自定义展示样式',
handleControl: '手动控制',
ariaBasic: 'ARIA基础用法',
},
'zh-TW': {
basic: '基础用法',
Expand All @@ -33,6 +35,7 @@ const CountDownDemo = () => {
controlTime: '控製開始和暫停的倒計時',
customStyle: '自定義展示樣式',
handleControl: '手動控製',
ariaBasic: 'ARIA基础用法',
},
'en-US': {
basic: 'Basic Usage',
Expand All @@ -44,6 +47,7 @@ const CountDownDemo = () => {
controlTime: 'Manual Control',
customStyle: 'Custom Style',
handleControl: 'Handle Control',
ariaBasic: 'ARIA Basic Usage',
},
})

Expand All @@ -68,6 +72,8 @@ const CountDownDemo = () => {
<Demo8 />
<h2>{translated.handleControl}</h2>
<Demo9 />
<h2>{translated.ariaBasic}</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
4 changes: 4 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,
ariaModal,
} = { ...defaultContentProps, ...props }

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

const classPrefix = 'nut-dialog'
Expand Down Expand Up @@ -58,6 +59,8 @@ export const Content: FunctionComponent<
className={classNames(`${classPrefix}-outer`, className)}
style={style}
onClick={handleClick}
role={role}
aria-modal={visible}
>
{close}
{header}
Expand Down
5 changes: 5 additions & 0 deletions src/packages/dialog/dialog.taro.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const defaultProps = {
onClose: () => {},
onConfirm: () => {},
onOverlayClick: () => true,
role: 'dialog',
} as TaroDialogProps

export const BaseDialog: FunctionComponent<Partial<TaroDialogProps>> & {
Expand Down Expand Up @@ -84,6 +85,7 @@ export const BaseDialog: FunctionComponent<Partial<TaroDialogProps>> & {
onCancel,
onConfirm,
onOverlayClick,
role,
},
setParams,
} = useParams(mergeProps(defaultProps, props))
Expand Down Expand Up @@ -283,6 +285,7 @@ export const BaseDialog: FunctionComponent<Partial<TaroDialogProps>> & {
footer={renderFooter()}
footerDirection={footerDirection}
visible={visible}
ariaRole={role}
>
{content || children}
</Content>
Expand All @@ -295,6 +298,7 @@ export const BaseDialog: FunctionComponent<Partial<TaroDialogProps>> & {
style={{ display: visible ? 'block' : 'none' }}
ref={refObject}
catchMove={lockScroll}
ariaModal={visible}
>
{overlay && (
<Overlay
Expand All @@ -305,6 +309,7 @@ export const BaseDialog: FunctionComponent<Partial<TaroDialogProps>> & {
closeOnOverlayClick={closeOnOverlayClick}
lockScroll={lockScroll}
onClick={onHandleClickOverlay}
ariaRoledescription="背景蒙层"
/>
)}
{renderContent()}
Expand Down
Loading