fix: add input validation and safe JSON parsing across 13 CLIs

Add missing parameter validation to prevent /path/undefined API calls:
- dub: require --url for links create
- google-search-console: require --sitemap-url for sitemaps submit
- kit: validate IDs and emails for subscribers, forms, sequences, tags, broadcasts
- mailchimp: validate IDs for lists get, campaigns get/create/send, members add, reports get
- resend: validate --from/--to/--subject for send, audience/contact IDs for contacts
- rewardful: validate IDs for affiliates get/update, commissions get, links create
- semrush: require --domain/--phrase for all domain and keyword commands
- sendgrid: validate --from/--to/--subject for send, campaign IDs, email for validate

Wrap bare JSON.parse() calls in try/catch for user-provided JSON:
- dub (--links), ga4 (--params), kit (--fields x4), mixpanel (--properties x2),
  onesignal (--filters), paddle (--scheduled-change, --items x2),
  resend (--emails, --variables x2), segment (--properties, --traits, --events),
  sendgrid (--template-data)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Corey Haines 2026-02-17 22:44:06 -08:00
parent 51bdf2f6b3
commit 8eaff5e29f
13 changed files with 103 additions and 20 deletions

View file

@ -58,6 +58,7 @@ async function main() {
case 'links':
switch (sub) {
case 'create': {
if (!args.url) { result = { error: '--url required' }; break }
const body = {}
if (args.url) body.url = args.url
if (args.domain) body.domain = args.domain
@ -95,7 +96,12 @@ async function main() {
result = await api('DELETE', `/links/${args.id}`)
break
case 'bulk-create': {
const links = JSON.parse(args.links || '[]')
let links
try {
links = JSON.parse(args.links || '[]')
} catch {
result = { error: 'Invalid JSON in --links' }; break
}
result = await api('POST', '/links/bulk', links)
break
}

View file

@ -150,7 +150,14 @@ async function main() {
if (!args['api-secret']) { result = { error: '--api-secret required' }; break }
if (!args['client-id']) { result = { error: '--client-id required' }; break }
if (!args['event-name']) { result = { error: '--event-name required' }; break }
const eventParams = args.params ? JSON.parse(args.params) : {}
let eventParams = {}
if (args.params) {
try {
eventParams = JSON.parse(args.params)
} catch {
result = { error: 'Invalid JSON in --params' }; break
}
}
const body = {
client_id: args['client-id'],
events: [{

View file

@ -129,6 +129,7 @@ async function main() {
result = await api('GET', `/webmasters/v3/sites/${encodedSiteUrl}/sitemaps`)
break
case 'submit': {
if (!args['sitemap-url']) { result = { error: '--sitemap-url required' }; break }
const sitemapUrl = encodeURIComponent(args['sitemap-url'])
result = await api('PUT', `/webmasters/v3/sites/${encodedSiteUrl}/sitemaps/${sitemapUrl}`)
if (!result.body && !result.error) {

View file

@ -91,12 +91,16 @@ async function main() {
break
}
case 'get':
if (!rest[0]) { result = { error: 'Subscriber ID required' }; break }
result = await api('GET', `/subscribers/${rest[0]}`)
break
case 'update': {
if (!rest[0]) { result = { error: 'Subscriber ID required' }; break }
const body = {}
if (args['first-name']) body.first_name = args['first-name']
if (args.fields) body.fields = JSON.parse(args.fields)
if (args.fields) {
try { body.fields = JSON.parse(args.fields) } catch { result = { error: 'Invalid JSON in --fields' }; break }
}
result = await api('PUT', `/subscribers/${rest[0]}`, body)
break
}
@ -116,10 +120,14 @@ async function main() {
result = await api('GET', '/forms', null, false)
break
case 'subscribe': {
if (!rest[0]) { result = { error: 'Form ID required' }; break }
if (!args.email) { result = { error: '--email required' }; break }
const formId = rest[0]
const body = { email: args.email }
if (args['first-name']) body.first_name = args['first-name']
if (args.fields) body.fields = JSON.parse(args.fields)
if (args.fields) {
try { body.fields = JSON.parse(args.fields) } catch { result = { error: 'Invalid JSON in --fields' }; break }
}
result = await api('POST', `/forms/${formId}/subscribe`, body, false)
break
}
@ -134,10 +142,14 @@ async function main() {
result = await api('GET', '/sequences', null, false)
break
case 'subscribe': {
if (!rest[0]) { result = { error: 'Sequence ID required' }; break }
if (!args.email) { result = { error: '--email required' }; break }
const sequenceId = rest[0]
const body = { email: args.email }
if (args['first-name']) body.first_name = args['first-name']
if (args.fields) body.fields = JSON.parse(args.fields)
if (args.fields) {
try { body.fields = JSON.parse(args.fields) } catch { result = { error: 'Invalid JSON in --fields' }; break }
}
result = await api('POST', `/sequences/${sequenceId}/subscribe`, body, false)
break
}
@ -152,16 +164,22 @@ async function main() {
result = await api('GET', '/tags', null, false)
break
case 'subscribe': {
if (!rest[0]) { result = { error: 'Tag ID required' }; break }
if (!args.email) { result = { error: '--email required' }; break }
const tagId = rest[0]
const body = { email: args.email }
if (args['first-name']) body.first_name = args['first-name']
if (args.fields) body.fields = JSON.parse(args.fields)
if (args.fields) {
try { body.fields = JSON.parse(args.fields) } catch { result = { error: 'Invalid JSON in --fields' }; break }
}
result = await api('POST', `/tags/${tagId}/subscribe`, body, false)
break
}
case 'remove': {
if (!rest[0]) { result = { error: 'Tag ID required' }; break }
const tagId = rest[0]
const subscriberId = rest[1] || args['subscriber-id']
if (!subscriberId) { result = { error: 'Subscriber ID required' }; break }
result = await api('DELETE', `/subscribers/${subscriberId}/tags/${tagId}`)
break
}
@ -178,6 +196,7 @@ async function main() {
break
}
case 'create': {
if (!args.subject || !args.content) { result = { error: '--subject and --content required' }; break }
const body = {
subject: args.subject,
content: args.content,

View file

@ -69,6 +69,7 @@ async function main() {
break
}
case 'get':
if (!rest[0]) { result = { error: 'List ID required' }; break }
result = await api('GET', `/lists/${rest[0]}`)
break
default:
@ -91,6 +92,8 @@ async function main() {
break
}
case 'add': {
if (!rest[0]) { result = { error: 'List ID required' }; break }
if (!args.email) { result = { error: '--email required' }; break }
if (!args['list-id']) {
result = { error: '--list-id is required for members add' }
break
@ -142,9 +145,11 @@ async function main() {
break
}
case 'get':
if (!rest[0]) { result = { error: 'Campaign ID required' }; break }
result = await api('GET', `/campaigns/${rest[0]}`)
break
case 'create': {
if (!args['list-id']) { result = { error: '--list-id required' }; break }
const body = {
type: args.type || 'regular',
recipients: {
@ -160,6 +165,7 @@ async function main() {
break
}
case 'send':
if (!rest[0]) { result = { error: 'Campaign ID required' }; break }
result = await api('POST', `/campaigns/${rest[0]}/actions/send`)
break
default:
@ -170,6 +176,7 @@ async function main() {
case 'reports':
switch (sub) {
case 'get':
if (!rest[0]) { result = { error: 'Campaign ID required' }; break }
result = await api('GET', `/reports/${rest[0]}`)
break
default:

View file

@ -118,7 +118,8 @@ async function main() {
if (!TOKEN) { result = { error: 'MIXPANEL_TOKEN required for tracking' }; break }
if (!args['distinct-id']) { result = { error: '--distinct-id required' }; break }
if (!args.event) { result = { error: '--event required' }; break }
const properties = args.properties ? JSON.parse(args.properties) : {}
let properties
try { properties = args.properties ? JSON.parse(args.properties) : {} } catch { result = { error: 'Invalid JSON in --properties' }; break }
properties.token = TOKEN
properties.distinct_id = args['distinct-id']
result = await ingestApi('POST', '/track', [{
@ -137,7 +138,8 @@ async function main() {
case 'set': {
if (!TOKEN) { result = { error: 'MIXPANEL_TOKEN required for profiles' }; break }
if (!args['distinct-id']) { result = { error: '--distinct-id required' }; break }
const properties = args.properties ? JSON.parse(args.properties) : {}
let properties
try { properties = args.properties ? JSON.parse(args.properties) : {} } catch { result = { error: 'Invalid JSON in --properties' }; break }
result = await ingestApi('POST', '/engage', [{
$token: TOKEN,
$distinct_id: args['distinct-id'],

View file

@ -130,7 +130,8 @@ async function main() {
case 'create': {
const name = args.name
if (!name) { result = { error: '--name required' }; break }
const filters = args.filters ? JSON.parse(args.filters) : [{ field: 'session_count', relation: '>', value: '0' }]
let filters
try { filters = args.filters ? JSON.parse(args.filters) : [{ field: 'session_count', relation: '>', value: '0' }] } catch { result = { error: 'Invalid JSON in --filters' }; break }
result = await api('POST', `/api/v1/apps/${APP_ID}/segments`, { name, filters })
break
}

View file

@ -203,7 +203,9 @@ async function main() {
if (!id) { result = { error: '--id required' }; break }
const body = {}
if (args['proration-billing-mode']) body.proration_billing_mode = args['proration-billing-mode']
if (args['scheduled-change']) body.scheduled_change = JSON.parse(args['scheduled-change'])
if (args['scheduled-change']) {
try { body.scheduled_change = JSON.parse(args['scheduled-change']) } catch { result = { error: 'Invalid JSON in --scheduled-change' }; break }
}
result = await api('PATCH', `/subscriptions/${id}`, body)
break
}
@ -248,7 +250,9 @@ async function main() {
case 'create': {
const items = args.items
if (!items) { result = { error: '--items required (JSON array of {price_id, quantity})' }; break }
const body = { items: JSON.parse(items) }
let parsedItems
try { parsedItems = JSON.parse(items) } catch { result = { error: 'Invalid JSON in --items' }; break }
const body = { items: parsedItems }
if (args['customer-id']) body.customer_id = args['customer-id']
result = await api('POST', '/transactions', body)
break
@ -304,11 +308,13 @@ async function main() {
if (!action) { result = { error: '--action required (refund, credit, chargeback)' }; break }
if (!reason) { result = { error: '--reason required' }; break }
if (!items) { result = { error: '--items required (JSON array of {item_id, type, amount})' }; break }
let parsedItems
try { parsedItems = JSON.parse(items) } catch { result = { error: 'Invalid JSON in --items' }; break }
result = await api('POST', '/adjustments', {
transaction_id: transactionId,
action,
reason,
items: JSON.parse(items),
items: parsedItems,
})
break
}

View file

@ -56,6 +56,7 @@ async function main() {
switch (cmd) {
case 'send': {
if (!args.from || !args.to || !args.subject) { result = { error: '--from, --to, and --subject required' }; break }
const body = { from: args.from, to: args.to?.split(','), subject: args.subject }
if (args.html) body.html = args.html
if (args.text) body.text = args.text
@ -156,6 +157,7 @@ async function main() {
case 'contacts': {
const audienceId = sub
if (!audienceId) { result = { error: 'Audience ID required as subcommand arg' }; break }
const action = rest[0]
const contactId = rest[1]
switch (action) {
@ -166,6 +168,7 @@ async function main() {
break
}
case 'get':
if (!rest[1]) { result = { error: 'Contact ID required' }; break }
result = await api('GET', `/audiences/${audienceId}/contacts/${contactId}`)
break
case 'create': {
@ -177,6 +180,7 @@ async function main() {
break
}
case 'update': {
if (!rest[1]) { result = { error: 'Contact ID required' }; break }
const body = {}
if (args['first-name']) body.first_name = args['first-name']
if (args['last-name']) body.last_name = args['last-name']
@ -185,6 +189,7 @@ async function main() {
break
}
case 'delete':
if (!rest[1]) { result = { error: 'Contact ID required' }; break }
result = await api('DELETE', `/audiences/${audienceId}/contacts/${contactId}`)
break
default:
@ -216,7 +221,12 @@ async function main() {
break
case 'batch': {
const emails = JSON.parse(args.emails || '[]')
let emails
try {
emails = JSON.parse(args.emails || '[]')
} catch (e) {
result = { error: 'Invalid JSON for --emails: ' + e.message }; break
}
result = await api('POST', '/emails/batch', emails)
break
}
@ -241,7 +251,9 @@ async function main() {
if (args.subject) body.subject = args.subject
if (args['reply-to']) body.reply_to = args['reply-to']
if (args.text) body.text = args.text
if (args.variables) body.variables = JSON.parse(args.variables)
if (args.variables) {
try { body.variables = JSON.parse(args.variables) } catch (e) { result = { error: 'Invalid JSON for --variables: ' + e.message }; break }
}
result = await api('POST', '/templates', body)
break
}
@ -254,7 +266,9 @@ async function main() {
if (args.subject) body.subject = args.subject
if (args['reply-to']) body.reply_to = args['reply-to']
if (args.text) body.text = args.text
if (args.variables) body.variables = JSON.parse(args.variables)
if (args.variables) {
try { body.variables = JSON.parse(args.variables) } catch (e) { result = { error: 'Invalid JSON for --variables: ' + e.message }; break }
}
result = await api('PATCH', `/templates/${rest[0]}`, body)
break
}

View file

@ -65,6 +65,7 @@ async function main() {
break
}
case 'get':
if (!rest[0]) { result = { error: 'Affiliate ID required' }; break }
result = await api('GET', `/affiliates/${rest[0]}`)
break
case 'search': {
@ -74,6 +75,7 @@ async function main() {
break
}
case 'update': {
if (!rest[0]) { result = { error: 'Affiliate ID required' }; break }
const body = {}
if (args['first-name']) body.first_name = args['first-name']
if (args['last-name']) body.last_name = args['last-name']
@ -114,6 +116,7 @@ async function main() {
break
}
case 'get':
if (!rest[0]) { result = { error: 'Commission ID required' }; break }
result = await api('GET', `/commissions/${rest[0]}`)
break
default:
@ -124,6 +127,7 @@ async function main() {
case 'links':
switch (sub) {
case 'create': {
if (!args['affiliate-id']) { result = { error: '--affiliate-id required' }; break }
const body = {}
if (args.token) body.token = args.token
if (args.url) body.url = args.url

View file

@ -93,7 +93,9 @@ async function main() {
userId: args['user-id'],
event: args.event,
}
if (args.properties) body.properties = JSON.parse(args.properties)
if (args.properties) {
try { body.properties = JSON.parse(args.properties) } catch { result = { error: 'Invalid JSON in --properties' }; break }
}
result = await trackApi('POST', '/track', body)
break
}
@ -107,7 +109,9 @@ async function main() {
case 'user': {
if (!args['user-id']) { result = { error: '--user-id required' }; break }
const body = { userId: args['user-id'] }
if (args.traits) body.traits = JSON.parse(args.traits)
if (args.traits) {
try { body.traits = JSON.parse(args.traits) } catch { result = { error: 'Invalid JSON in --traits' }; break }
}
result = await trackApi('POST', '/identify', body)
break
}
@ -122,7 +126,9 @@ async function main() {
if (!args['user-id']) { result = { error: '--user-id required' }; break }
const body = { userId: args['user-id'] }
if (args.name) body.name = args.name
if (args.properties) body.properties = JSON.parse(args.properties)
if (args.properties) {
try { body.properties = JSON.parse(args.properties) } catch { result = { error: 'Invalid JSON in --properties' }; break }
}
result = await trackApi('POST', '/page', body)
break
}
@ -135,7 +141,8 @@ async function main() {
switch (sub) {
case 'send': {
if (!args.events) { result = { error: '--events required (JSON array)' }; break }
const batch = JSON.parse(args.events)
let batch
try { batch = JSON.parse(args.events) } catch { result = { error: 'Invalid JSON in --events' }; break }
result = await trackApi('POST', '/batch', { batch })
break
}

View file

@ -75,6 +75,7 @@ async function main() {
case 'domain':
switch (sub) {
case 'overview': {
if (!args.domain) { result = { error: '--domain required' }; break }
const params = new URLSearchParams({
type: 'domain_ranks',
export_columns: 'Db,Dn,Rk,Or,Ot,Oc,Ad,At,Ac',
@ -84,6 +85,7 @@ async function main() {
break
}
case 'organic': {
if (!args.domain) { result = { error: '--domain required' }; break }
const params = new URLSearchParams({
type: 'domain_organic',
export_columns: 'Ph,Po,Pp,Pd,Nq,Cp,Ur,Tr,Tc,Co,Nr',
@ -95,6 +97,7 @@ async function main() {
break
}
case 'competitors': {
if (!args.domain) { result = { error: '--domain required' }; break }
const params = new URLSearchParams({
type: 'domain_organic_organic',
export_columns: 'Dn,Cr,Np,Or,Ot,Oc,Ad',
@ -113,6 +116,7 @@ async function main() {
case 'keywords':
switch (sub) {
case 'overview': {
if (!args.phrase) { result = { error: '--phrase required' }; break }
const params = new URLSearchParams({
type: 'phrase_all',
export_columns: 'Ph,Nq,Cp,Co,Nr',
@ -123,6 +127,7 @@ async function main() {
break
}
case 'related': {
if (!args.phrase) { result = { error: '--phrase required' }; break }
const params = new URLSearchParams({
type: 'phrase_related',
export_columns: 'Ph,Nq,Cp,Co,Nr,Td',
@ -134,6 +139,7 @@ async function main() {
break
}
case 'difficulty': {
if (!args.phrase) { result = { error: '--phrase required' }; break }
const params = new URLSearchParams({
type: 'phrase_kdi',
export_columns: 'Ph,Kd',

View file

@ -56,6 +56,7 @@ async function main() {
switch (cmd) {
case 'send': {
if (!args.from || !args.to || !args.subject) { result = { error: '--from, --to, and --subject required' }; break }
const body = {
personalizations: [{
to: args.to.split(',').map(e => ({ email: e.trim() })),
@ -66,7 +67,7 @@ async function main() {
if (args['template-id']) {
body.template_id = args['template-id']
if (args['template-data']) {
body.personalizations[0].dynamic_template_data = JSON.parse(args['template-data'])
try { body.personalizations[0].dynamic_template_data = JSON.parse(args['template-data']) } catch (e) { result = { error: 'Invalid JSON for --template-data: ' + e.message }; break }
}
} else {
const content = []
@ -117,6 +118,7 @@ async function main() {
break
}
case 'get':
if (!rest[0]) { result = { error: 'Campaign ID required' }; break }
result = await api('GET', `/marketing/campaigns/${rest[0]}`)
break
default:
@ -172,6 +174,7 @@ async function main() {
case 'validate':
switch (sub) {
case 'email': {
if (!args.email && !rest[0]) { result = { error: '--email required' }; break }
const body = { email: args.email || rest[0] }
result = await api('POST', '/validations/email', body)
break