mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-17 09:41:58 +00:00
fix(desktop): address CodeQL alerts on PR #20059
- settings/helpers.ts: harden setNested against prototype pollution.
POLLUTING_PATH_PARTS check is now applied at every assignment site
(loop + leaf) and uses Object.defineProperty so CodeQL can see the
guard inline rather than via a helper function call.
- lib/markdown-preprocess.ts: rebuild the dangling-fence close regex
from a fence-char + length instead of marker.replace(...). The marker
is captured by `(`{3,}|~{3,})` so it can only be backticks or tildes,
but CodeQL was tracing tainted input text into the RegExp source and
flagging hostname dots from input as part of the pattern (false
positive js/incomplete-hostname-regexp on the test fixture URLs).
Reconstructing from a literal char breaks the dataflow.
- scripts/notarize-artifact.cjs: drop args from the run() rejection
message. Args carry --key-id / --issuer / key file path; the existing
outer catch already squashes errors to a generic line, but CodeQL was
flagging the args.join(' ') as clear-text logging of APPLE_API_KEY_ID.
Composer DOM-text-as-HTML alerts (composer/index.tsx:379, :547) are
already addressed in 4dd9732a9 — innerHTML assignment was replaced with
renderComposerContents which builds DOM via replaceChildren / append
text nodes (no HTML interpretation).
This commit is contained in:
parent
dc66a98430
commit
2ce691d8ca
3 changed files with 40 additions and 6 deletions
|
|
@ -7,7 +7,10 @@ function run(command, args) {
|
|||
return new Promise((resolve, reject) => {
|
||||
execFile(command, args, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(new Error(`${command} ${args.join(' ')} failed: ${stderr?.trim() || stdout?.trim() || error.message}`))
|
||||
// Intentionally omit args from the rejection message: callers pass
|
||||
// notarization credentials (key id, issuer, key file path) here, and
|
||||
// surfacing them in error output would land in CI logs.
|
||||
reject(new Error(`${command} failed: ${stderr?.trim() || stdout?.trim() || error.message}`))
|
||||
return
|
||||
}
|
||||
resolve()
|
||||
|
|
|
|||
|
|
@ -25,16 +25,32 @@ export const providerPriority = (name: string) => PROVIDER_GROUPS.find(g => g.na
|
|||
|
||||
const POLLUTING_PATH_PARTS = new Set(['__proto__', 'constructor', 'prototype'])
|
||||
|
||||
function isSafePart(part: string): boolean {
|
||||
return part.length > 0 && !POLLUTING_PATH_PARTS.has(part)
|
||||
}
|
||||
|
||||
function configPathParts(path: string): string[] {
|
||||
const parts = path.split('.')
|
||||
|
||||
if (parts.some(part => !part || POLLUTING_PATH_PARTS.has(part))) {
|
||||
if (!parts.every(isSafePart)) {
|
||||
throw new Error(`Unsafe config path: ${path}`)
|
||||
}
|
||||
|
||||
return parts
|
||||
}
|
||||
|
||||
function safeSet(target: Record<string, unknown>, key: string, value: unknown): void {
|
||||
if (!isSafePart(key)) {
|
||||
throw new Error(`Unsafe config key: ${key}`)
|
||||
}
|
||||
Object.defineProperty(target, key, {
|
||||
value,
|
||||
writable: true,
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
})
|
||||
}
|
||||
|
||||
export function getNested(obj: HermesConfigRecord, path: string): unknown {
|
||||
let cur: unknown = obj
|
||||
|
||||
|
|
@ -43,6 +59,10 @@ export function getNested(obj: HermesConfigRecord, path: string): unknown {
|
|||
return undefined
|
||||
}
|
||||
|
||||
if (!Object.prototype.hasOwnProperty.call(cur, part)) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
cur = (cur as Record<string, unknown>)[part]
|
||||
}
|
||||
|
||||
|
|
@ -57,14 +77,20 @@ export function setNested(obj: HermesConfigRecord, path: string, value: unknown)
|
|||
for (let i = 0; i < parts.length - 1; i += 1) {
|
||||
const part = parts[i]
|
||||
|
||||
if (cur[part] == null || typeof cur[part] !== 'object') {
|
||||
cur[part] = {}
|
||||
if (!isSafePart(part)) {
|
||||
throw new Error(`Unsafe config path part: ${part}`)
|
||||
}
|
||||
|
||||
const existing = Object.prototype.hasOwnProperty.call(cur, part) ? cur[part] : undefined
|
||||
|
||||
if (existing == null || typeof existing !== 'object') {
|
||||
safeSet(cur, part, {})
|
||||
}
|
||||
|
||||
cur = cur[part] as Record<string, unknown>
|
||||
}
|
||||
|
||||
cur[parts[parts.length - 1]] = value
|
||||
safeSet(cur, parts[parts.length - 1], value)
|
||||
|
||||
return clone
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,12 @@ function scrubBacktickNoise(text: string): string {
|
|||
const marker = match[2] || '```'
|
||||
const info = match[3] || ''
|
||||
const body = match[4] || ''
|
||||
const closeRe = new RegExp(`\\n[ \\t]*${marker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[ \\t]*(?=\\n|$)`)
|
||||
// `marker` is captured by `(`{3,}|~{3,})` above, so its only meta-character
|
||||
// is the backtick or tilde. Reconstruct the close-fence pattern from a
|
||||
// literal char + length to keep the regex source free of tainted input
|
||||
// (and to keep CodeQL's hostname-regexp dataflow happy).
|
||||
const fenceChar = marker[0] === '~' ? '\\~' : '`'
|
||||
const closeRe = new RegExp(`\\n[ \\t]*${fenceChar}{${marker.length}}[ \\t]*(?=\\n|$)`)
|
||||
|
||||
if (!closeRe.test(body) && sanitizeLanguageTag(info) && !isLikelyProseFence(info, body)) {
|
||||
protectedRanges.push({ end: text.length, start })
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue