Skip to content

Commit 7cd6cac

Browse files
committed
fix(db): add security validation to studio proxy
- Validate port as number to prevent XSS injection - Validate path to prevent SSRF and path traversal - Add try/catch for HTML fetch - Add missing content types for png/ico - Simplify D1/libsql driver selection logic
1 parent 8ee530c commit 7cd6cac

2 files changed

Lines changed: 30 additions & 19 deletions

File tree

src/db/runtime/api/studio.get.dev.ts

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,29 @@
11
// Proxy Drizzle Studio frontend to avoid CORS/Private Network Access issues
22
// Serving from localhost allows connecting to localhost:4983 without browser blocking
33

4-
export default defineEventHandler(async (event) => {
4+
import { eventHandler, getQuery, setHeader, createError } from 'h3'
5+
6+
export default eventHandler(async (event) => {
57
const query = getQuery(event)
6-
const port = query.port || 4983
8+
9+
// Validate port to prevent XSS injection
10+
const port = parseInt(String(query.port)) || 4983
11+
if (port < 1 || port > 65535) {
12+
throw createError({ statusCode: 400, message: 'Invalid port' })
13+
}
14+
715
// Get path without query string
816
const fullPath = event.path?.split('?')[0] || ''
917
const path = fullPath.replace('/api/_hub/studio', '') || '/'
1018

19+
// Validate path to prevent SSRF and path traversal
20+
if (path !== '/' && (!/^\/[a-zA-Z0-9._-]+$/.test(path) || path.includes('..'))) {
21+
throw createError({ statusCode: 400, message: 'Invalid path' })
22+
}
1123

1224
// Proxy asset requests (JS, CSS, etc.)
1325
if (path !== '/' && path !== '') {
1426
try {
15-
// Use text for JS/CSS, blob for binary
1627
const isBinary = path.endsWith('.svg') || path.endsWith('.png') || path.endsWith('.ico')
1728
const asset = await $fetch(`https://local.drizzle.studio${path}`, {
1829
responseType: isBinary ? 'blob' : 'text'
@@ -22,17 +33,24 @@ export default defineEventHandler(async (event) => {
2233
if (path.endsWith('.js')) setHeader(event, 'Content-Type', 'application/javascript')
2334
else if (path.endsWith('.css')) setHeader(event, 'Content-Type', 'text/css')
2435
else if (path.endsWith('.svg')) setHeader(event, 'Content-Type', 'image/svg+xml')
36+
else if (path.endsWith('.png')) setHeader(event, 'Content-Type', 'image/png')
37+
else if (path.endsWith('.ico')) setHeader(event, 'Content-Type', 'image/x-icon')
2538

2639
return asset
27-
} catch (error: any) {
28-
throw createError({ statusCode: 502, message: `Failed to fetch studio asset: ${path}` })
40+
} catch {
41+
throw createError({ statusCode: 502, message: 'Failed to fetch studio asset' })
2942
}
3043
}
3144

3245
// Fetch the Drizzle Studio HTML
33-
const html = await $fetch<string>('https://local.drizzle.studio/', {
34-
headers: { 'Accept': 'text/html' }
35-
})
46+
let html: string
47+
try {
48+
html = await $fetch<string>('https://local.drizzle.studio/', {
49+
headers: { 'Accept': 'text/html' }
50+
})
51+
} catch {
52+
throw createError({ statusCode: 502, message: 'Failed to fetch Drizzle Studio' })
53+
}
3654

3755
// Rewrite asset paths to go through our proxy
3856
let modifiedHtml = html

src/db/setup.ts

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -49,19 +49,12 @@ export async function resolveDatabaseConfig(nuxt: Nuxt, hub: HubConfig): Promise
4949
}
5050
break
5151
}
52-
// Cloudflare D1
53-
if (hub.hosting.includes('cloudflare')) {
54-
if (nuxt.options.dev) {
55-
// Use libsql in dev mode since D1 bindings don't exist locally
56-
config.driver = 'libsql'
57-
config.connection = defu(config.connection, { url: `file:${join(hub.dir!, 'db/sqlite.db')}` })
58-
await mkdir(join(hub.dir, 'db'), { recursive: true })
59-
} else {
60-
config.driver = 'd1'
61-
}
52+
// Cloudflare D1 (production only - dev mode uses local libsql)
53+
if (hub.hosting.includes('cloudflare') && !nuxt.options.dev) {
54+
config.driver = 'd1'
6255
break
6356
}
64-
// Local SQLite
57+
// Local SQLite (libsql) - used for local dev including cloudflare preset
6558
config.driver ||= 'libsql'
6659
config.connection = defu(config.connection, { url: `file:${join(hub.dir!, 'db/sqlite.db')}` })
6760
await mkdir(join(hub.dir, 'db'), { recursive: true })

0 commit comments

Comments
 (0)