@goodandready/dsh-image-gen 0.11.18 → 0.11.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -317,6 +317,20 @@ export function optimizeSvgContent(svgString, options = {}) {
317
317
  cleaned = cleaned.replace(/\s*(?:xmlns:inkscape|xmlns:sodipodi|xmlns:sketch|inkscape:[a-z-]+|sodipodi:[a-z-]+|sketch:[a-z-]+)="[^"]*"/gi, '')
318
318
  cleaned = cleaned.replace(/<(?:metadata|sodipodi:namedview)[\s\S]*?<\/(?:metadata|sodipodi:namedview)>/gi, '')
319
319
 
320
+ // Security (#374): Strip active/dangerous SVG tags (<script>, <foreignObject>, <iframe>, <embed>, <object>, <applet>)
321
+ cleaned = cleaned.replace(/<script[\s\S]*?<\/script>/gi, '')
322
+ cleaned = cleaned.replace(/<script[^>]*\/?>/gi, '')
323
+ cleaned = cleaned.replace(/<foreignObject[\s\S]*?<\/foreignObject>/gi, '')
324
+ cleaned = cleaned.replace(/<foreignObject[^>]*\/?>/gi, '')
325
+ cleaned = cleaned.replace(/<(?:iframe|embed|object|applet)[\s\S]*?<\/(?:iframe|embed|object|applet)>/gi, '')
326
+ cleaned = cleaned.replace(/<(?:iframe|embed|object|applet)[^>]*\/?>/gi, '')
327
+
328
+ // Strip event handler attributes (onload, onerror, onclick, etc.)
329
+ cleaned = cleaned.replace(/\s+on[a-z]+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi, '')
330
+
331
+ // Strip dangerous URI schemes (javascript:, data:text/html, vbscript:) in href/xlink:href/src/action
332
+ cleaned = cleaned.replace(/\s+(?:href|xlink:href|src|action)\s*=\s*(?:"\s*(?:javascript|data:text\/html|vbscript):[^"]*"|'\s*(?:javascript|data:text\/html|vbscript):[^']*')/gi, '')
333
+
320
334
  cleaned = cleaned.replace(/\s{2,}/g, ' ')
321
335
  cleaned = cleaned.replace(/>\s+</g, '><')
322
336
 
@@ -1,4 +1,4 @@
1
- import { readFile, stat } from 'node:fs/promises'
1
+ import { readFile, stat, realpath } from 'node:fs/promises'
2
2
  import path from 'node:path'
3
3
  import { estimateSharpnessAndVariance } from './providers.js'
4
4
 
@@ -186,14 +186,24 @@ export async function resolveConversationImage(ctx, exec, targetRef) {
186
186
  }
187
187
 
188
188
  // 3. Local filesystem resolution with strict workspace containment check
189
- const root = sessionCwd ? path.resolve(sessionCwd) : process.cwd()
189
+ const root = path.resolve(sessionCwd ? sessionCwd : process.cwd())
190
190
  const absPath = path.resolve(root, effectiveRef)
191
191
  const rel = path.relative(root, absPath)
192
192
  if (rel.startsWith('..') || path.isAbsolute(rel)) {
193
193
  throw new Error(`Access denied: path "${effectiveRef}" is outside allowed workspace directory "${root}"`)
194
194
  }
195
195
 
196
- const fileStat = await stat(absPath).catch(() => null)
196
+ const realRoot = await realpath(root).catch(() => root)
197
+ const realTarget = await realpath(absPath).catch(() => null)
198
+ if (!realTarget) {
199
+ throw new Error(`Image file not found: ${effectiveRef}`)
200
+ }
201
+ const relReal = path.relative(realRoot, realTarget)
202
+ if (relReal.startsWith('..') || path.isAbsolute(relReal)) {
203
+ throw new Error(`Access denied: path "${effectiveRef}" points outside allowed workspace directory "${realRoot}"`)
204
+ }
205
+
206
+ const fileStat = await stat(realTarget).catch(() => null)
197
207
  if (!fileStat) {
198
208
  throw new Error(`Image file not found: ${effectiveRef}`)
199
209
  }
@@ -208,7 +218,7 @@ export async function resolveConversationImage(ctx, exec, targetRef) {
208
218
  throw new Error(`Image file too large (${(fileStat.size / (1024 * 1024)).toFixed(1)} MB). Maximum allowed size is 50 MB.`)
209
219
  }
210
220
 
211
- const bytes = await readFile(absPath)
221
+ const bytes = await readFile(realTarget)
212
222
  validateImageSignature(bytes, effectiveRef)
213
223
  const mediaType = detectMediaType(absPath, bytes)
214
224
  return {
@@ -1,7 +1,7 @@
1
1
  // lib/resolve-source.js — Safe image source resolution for image tools (#317)
2
2
 
3
3
  import path from 'node:path'
4
- import { readFile, stat } from 'node:fs/promises'
4
+ import { readFile, stat, realpath } from 'node:fs/promises'
5
5
  import { validateImageSignature } from './resolve-image.js'
6
6
 
7
7
  /** Read source image for editing: filesystem path or attachment id. */
@@ -41,7 +41,17 @@ export async function resolveSource(ctx, exec, ref) {
41
41
  throw new Error(`Access denied: path "${ref}" is outside allowed workspace directory "${root}"`)
42
42
  }
43
43
 
44
- const fileStat = await stat(target).catch(() => null)
44
+ const realRoot = await realpath(root).catch(() => root)
45
+ const realTarget = await realpath(target).catch(() => null)
46
+ if (!realTarget) {
47
+ throw new Error(`Source image not found: ${ref}`)
48
+ }
49
+ const relReal = path.relative(realRoot, realTarget)
50
+ if (relReal.startsWith('..') || path.isAbsolute(relReal)) {
51
+ throw new Error(`Access denied: path "${ref}" points outside allowed workspace directory "${realRoot}"`)
52
+ }
53
+
54
+ const fileStat = await stat(realTarget).catch(() => null)
45
55
  if (!fileStat) {
46
56
  throw new Error(`Source image not found: ${ref}`)
47
57
  }
@@ -55,7 +65,7 @@ export async function resolveSource(ctx, exec, ref) {
55
65
  throw new Error(`Source image too large (${(fileStat.size / (1024 * 1024)).toFixed(1)} MB). Maximum allowed size is 50 MB.`)
56
66
  }
57
67
 
58
- const bytes = await readFile(target)
68
+ const bytes = await readFile(realTarget)
59
69
  validateImageSignature(bytes, ref)
60
70
  const ext = path.extname(target).toLowerCase()
61
71
  const mediaType = ext === '.jpg' || ext === '.jpeg' ? 'image/jpeg'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-image-gen",
3
- "version": "0.11.18",
3
+ "version": "0.11.19",
4
4
  "description": "Image generation for DeepSeek Harness: a generate_image tool with pluggable providers — the FAL queue, any OpenAI-compatible images API, or a ChatGPT/Grok subscription with no API key at all. The picture is shown inline in the conversation; the model receives either a link (works with any chat model) or the image itself (needs dsh-vision-bridge or a vision-capable model).",
5
5
  "keywords": [
6
6
  "deepseek-harness",