@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.
- package/lib/frontend-assets.js +14 -0
- package/lib/resolve-image.js +14 -4
- package/lib/resolve-source.js +13 -3
- package/package.json +1 -1
package/lib/frontend-assets.js
CHANGED
|
@@ -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
|
|
package/lib/resolve-image.js
CHANGED
|
@@ -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 =
|
|
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
|
|
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(
|
|
221
|
+
const bytes = await readFile(realTarget)
|
|
212
222
|
validateImageSignature(bytes, effectiveRef)
|
|
213
223
|
const mediaType = detectMediaType(absPath, bytes)
|
|
214
224
|
return {
|
package/lib/resolve-source.js
CHANGED
|
@@ -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
|
|
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(
|
|
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.
|
|
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",
|