From 8eaff5e29f8d8d208ad89296f872f75caf38e126 Mon Sep 17 00:00:00 2001 From: Corey Haines <34802794+coreyhaines31@users.noreply.github.com> Date: Tue, 17 Feb 2026 22:44:06 -0800 Subject: [PATCH] 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 --- tools/clis/dub.js | 8 +++++++- tools/clis/ga4.js | 9 ++++++++- tools/clis/google-search-console.js | 1 + tools/clis/kit.js | 27 +++++++++++++++++++++++---- tools/clis/mailchimp.js | 7 +++++++ tools/clis/mixpanel.js | 6 ++++-- tools/clis/onesignal.js | 3 ++- tools/clis/paddle.js | 12 +++++++++--- tools/clis/resend.js | 20 +++++++++++++++++--- tools/clis/rewardful.js | 4 ++++ tools/clis/segment.js | 15 +++++++++++---- tools/clis/semrush.js | 6 ++++++ tools/clis/sendgrid.js | 5 ++++- 13 files changed, 103 insertions(+), 20 deletions(-) diff --git a/tools/clis/dub.js b/tools/clis/dub.js index dba6b93..5cad79e 100755 --- a/tools/clis/dub.js +++ b/tools/clis/dub.js @@ -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 } diff --git a/tools/clis/ga4.js b/tools/clis/ga4.js index 4dfd9ab..ed91886 100755 --- a/tools/clis/ga4.js +++ b/tools/clis/ga4.js @@ -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: [{ diff --git a/tools/clis/google-search-console.js b/tools/clis/google-search-console.js index ceecf8f..2264a18 100755 --- a/tools/clis/google-search-console.js +++ b/tools/clis/google-search-console.js @@ -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) { diff --git a/tools/clis/kit.js b/tools/clis/kit.js index d17abdb..3cfd3ee 100755 --- a/tools/clis/kit.js +++ b/tools/clis/kit.js @@ -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, diff --git a/tools/clis/mailchimp.js b/tools/clis/mailchimp.js index 9c41dbc..5add6aa 100755 --- a/tools/clis/mailchimp.js +++ b/tools/clis/mailchimp.js @@ -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: diff --git a/tools/clis/mixpanel.js b/tools/clis/mixpanel.js index cae1ad3..671ca45 100755 --- a/tools/clis/mixpanel.js +++ b/tools/clis/mixpanel.js @@ -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'], diff --git a/tools/clis/onesignal.js b/tools/clis/onesignal.js index 408ecd7..63431e1 100755 --- a/tools/clis/onesignal.js +++ b/tools/clis/onesignal.js @@ -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 } diff --git a/tools/clis/paddle.js b/tools/clis/paddle.js index cd8d9b8..0349981 100755 --- a/tools/clis/paddle.js +++ b/tools/clis/paddle.js @@ -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 } diff --git a/tools/clis/resend.js b/tools/clis/resend.js index 3e56f21..20c4851 100755 --- a/tools/clis/resend.js +++ b/tools/clis/resend.js @@ -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 } diff --git a/tools/clis/rewardful.js b/tools/clis/rewardful.js index 9da3bba..dbaf01e 100755 --- a/tools/clis/rewardful.js +++ b/tools/clis/rewardful.js @@ -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 diff --git a/tools/clis/segment.js b/tools/clis/segment.js index 963993a..d1c9d90 100755 --- a/tools/clis/segment.js +++ b/tools/clis/segment.js @@ -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 } diff --git a/tools/clis/semrush.js b/tools/clis/semrush.js index fe55b71..8ba985c 100755 --- a/tools/clis/semrush.js +++ b/tools/clis/semrush.js @@ -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', diff --git a/tools/clis/sendgrid.js b/tools/clis/sendgrid.js index 1fa1fa4..d5167fd 100755 --- a/tools/clis/sendgrid.js +++ b/tools/clis/sendgrid.js @@ -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