From c1be574c8bd2334d8c26dabcdfb75bf8b8ef55f8 Mon Sep 17 00:00:00 2001 From: Corey Haines <34802794+coreyhaines31@users.noreply.github.com> Date: Tue, 17 Feb 2026 22:39:16 -0800 Subject: [PATCH] =?UTF-8?q?fix:=20security=20hardening=20=E2=80=94=20move?= =?UTF-8?q?=20meta-ads=20to=20header=20auth,=20encode=20URLs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Critical: - meta-ads: move access_token from URL query string to Authorization header to prevent credential leakage in server logs and referrers Medium (URL encoding): - g2: encode state and date filter values - trustpilot: use URLSearchParams for reviews list params - typeform: encode response IDs in delete endpoint - demio: encode event type filter - lemlist: encode email addresses in URL path segments Docs: - Fix 6 missing env vars in CLI README auth table - Fix .gitignore typo (extra space in .DS_Store pattern) Co-Authored-By: Claude Opus 4.6 --- .gitignore | 2 +- tools/clis/README.md | 12 ++++++------ tools/clis/demio.js | 2 +- tools/clis/g2.js | 6 +++--- tools/clis/lemlist.js | 10 +++++----- tools/clis/meta-ads.js | 11 ++++++----- tools/clis/trustpilot.js | 15 ++++++++------- tools/clis/typeform.js | 2 +- 8 files changed, 31 insertions(+), 29 deletions(-) diff --git a/.gitignore b/.gitignore index 35fe046..d0ee8f4 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,7 @@ node_modules/ # macOS .DS_Store -**/. DS_Store +**/.DS_Store # macOS / iCloud duplicate files * 2.* diff --git a/tools/clis/README.md b/tools/clis/README.md index 53f85e8..792b1b0 100644 --- a/tools/clis/README.md +++ b/tools/clis/README.md @@ -37,7 +37,7 @@ Every CLI reads credentials from environment variables: | CLI | Environment Variable | |-----|---------------------| | `activecampaign` | `ACTIVECAMPAIGN_API_KEY`, `ACTIVECAMPAIGN_API_URL` | -| `adobe-analytics` | `ADOBE_CLIENT_ID`, `ADOBE_ACCESS_TOKEN` | +| `adobe-analytics` | `ADOBE_ACCESS_TOKEN`, `ADOBE_CLIENT_ID`, `ADOBE_COMPANY_ID` | | `ahrefs` | `AHREFS_API_KEY` | | `amplitude` | `AMPLITUDE_API_KEY`, `AMPLITUDE_SECRET_KEY` | | `apollo` | `APOLLO_API_KEY` | @@ -52,7 +52,7 @@ Every CLI reads credentials from environment variables: | `dub` | `DUB_API_KEY` | | `g2` | `G2_API_TOKEN` | | `ga4` | `GA4_ACCESS_TOKEN` | -| `google-ads` | `GOOGLE_ADS_TOKEN`, `GOOGLE_ADS_DEVELOPER_TOKEN` | +| `google-ads` | `GOOGLE_ADS_TOKEN`, `GOOGLE_ADS_DEVELOPER_TOKEN`, `GOOGLE_ADS_CUSTOMER_ID` | | `google-search-console` | `GSC_ACCESS_TOKEN` | | `hotjar` | `HOTJAR_CLIENT_ID`, `HOTJAR_CLIENT_SECRET` | | `intercom` | `INTERCOM_API_KEY` | @@ -63,13 +63,13 @@ Every CLI reads credentials from environment variables: | `livestorm` | `LIVESTORM_API_TOKEN` | | `mailchimp` | `MAILCHIMP_API_KEY` | | `mention-me` | `MENTIONME_API_KEY` | -| `meta-ads` | `META_ACCESS_TOKEN` | +| `meta-ads` | `META_ACCESS_TOKEN`, `META_AD_ACCOUNT_ID` | | `mixpanel` | `MIXPANEL_TOKEN` (ingestion), `MIXPANEL_API_KEY` + `MIXPANEL_SECRET` (query) | | `onesignal` | `ONESIGNAL_REST_API_KEY`, `ONESIGNAL_APP_ID` | | `optimizely` | `OPTIMIZELY_API_KEY` | -| `paddle` | `PADDLE_API_KEY` | +| `paddle` | `PADDLE_API_KEY`, `PADDLE_SANDBOX` (optional) | | `partnerstack` | `PARTNERSTACK_PUBLIC_KEY`, `PARTNERSTACK_SECRET_KEY` | -| `plausible` | `PLAUSIBLE_API_KEY` | +| `plausible` | `PLAUSIBLE_API_KEY`, `PLAUSIBLE_BASE_URL` (optional, for self-hosted) | | `postmark` | `POSTMARK_API_KEY` | | `resend` | `RESEND_API_KEY` | | `rewardful` | `REWARDFUL_API_KEY` | @@ -77,7 +77,7 @@ Every CLI reads credentials from environment variables: | `segment` | `SEGMENT_WRITE_KEY` (tracking), `SEGMENT_ACCESS_TOKEN` (profile) | | `semrush` | `SEMRUSH_API_KEY` | | `sendgrid` | `SENDGRID_API_KEY` | -| `tiktok-ads` | `TIKTOK_ACCESS_TOKEN` | +| `tiktok-ads` | `TIKTOK_ACCESS_TOKEN`, `TIKTOK_ADVERTISER_ID` | | `tolt` | `TOLT_API_KEY` | | `trustpilot` | `TRUSTPILOT_API_KEY`, `TRUSTPILOT_API_SECRET`, `TRUSTPILOT_BUSINESS_UNIT_ID` | | `typeform` | `TYPEFORM_API_KEY` | diff --git a/tools/clis/demio.js b/tools/clis/demio.js index 678200d..9e12082 100755 --- a/tools/clis/demio.js +++ b/tools/clis/demio.js @@ -72,7 +72,7 @@ async function main() { case 'list': { const type = args.type let qs = '' - if (type) qs = `?type=${type}` + if (type) qs = `?type=${encodeURIComponent(type)}` result = await api('GET', `/events${qs}`) break } diff --git a/tools/clis/g2.js b/tools/clis/g2.js index 7eabbd8..967ef6a 100755 --- a/tools/clis/g2.js +++ b/tools/clis/g2.js @@ -64,7 +64,7 @@ async function main() { case 'list': { let qs = `?page[size]=${perPage}&page[number]=${page}` if (productId) qs += `&filter[product_id]=${productId}` - if (args.state) qs += `&filter[state]=${args.state}` + if (args.state) qs += `&filter[state]=${encodeURIComponent(args.state)}` result = await api('GET', `/survey-responses${qs}`) break } @@ -152,8 +152,8 @@ async function main() { switch (sub) { case 'visitors': { let qs = `?page[size]=${perPage}&page[number]=${page}` - if (args.start) qs += `&filter[start_date]=${args.start}` - if (args.end) qs += `&filter[end_date]=${args.end}` + if (args.start) qs += `&filter[start_date]=${encodeURIComponent(args.start)}` + if (args.end) qs += `&filter[end_date]=${encodeURIComponent(args.end)}` result = await api('GET', `/tracking-events${qs}`) break } diff --git a/tools/clis/lemlist.js b/tools/clis/lemlist.js index 9732fb9..8225d86 100755 --- a/tools/clis/lemlist.js +++ b/tools/clis/lemlist.js @@ -112,7 +112,7 @@ async function main() { case 'get': { if (!args['campaign-id']) { result = { error: '--campaign-id required' }; break } if (!args.email) { result = { error: '--email required' }; break } - result = await api('GET', `/campaigns/${args['campaign-id']}/leads/${args.email}`) + result = await api('GET', `/campaigns/${args['campaign-id']}/leads/${encodeURIComponent(args.email)}`) break } case 'add': { @@ -122,13 +122,13 @@ async function main() { if (args['first-name']) body.firstName = args['first-name'] if (args['last-name']) body.lastName = args['last-name'] if (args.company) body.companyName = args.company - result = await api('POST', `/campaigns/${args['campaign-id']}/leads/${args.email}`, body) + result = await api('POST', `/campaigns/${args['campaign-id']}/leads/${encodeURIComponent(args.email)}`, body) break } case 'delete': { if (!args['campaign-id']) { result = { error: '--campaign-id required' }; break } if (!args.email) { result = { error: '--email required' }; break } - result = await api('DELETE', `/campaigns/${args['campaign-id']}/leads/${args.email}`) + result = await api('DELETE', `/campaigns/${args['campaign-id']}/leads/${encodeURIComponent(args.email)}`) break } default: @@ -147,12 +147,12 @@ async function main() { } case 'add': { if (!args.email) { result = { error: '--email required' }; break } - result = await api('POST', `/unsubscribes/${args.email}`) + result = await api('POST', `/unsubscribes/${encodeURIComponent(args.email)}`) break } case 'delete': { if (!args.email) { result = { error: '--email required' }; break } - result = await api('DELETE', `/unsubscribes/${args.email}`) + result = await api('DELETE', `/unsubscribes/${encodeURIComponent(args.email)}`) break } default: diff --git a/tools/clis/meta-ads.js b/tools/clis/meta-ads.js index 3261617..d19b028 100755 --- a/tools/clis/meta-ads.js +++ b/tools/clis/meta-ads.js @@ -10,16 +10,17 @@ if (!TOKEN) { } async function api(method, path, body) { - const separator = path.includes('?') ? '&' : '?' - const url = `${BASE_URL}${path}${separator}access_token=${TOKEN}` - const opts = { method, headers: {} } + const url = `${BASE_URL}${path}` + const opts = { + method, + headers: { 'Authorization': `Bearer ${TOKEN}` }, + } if (body) { opts.headers['Content-Type'] = 'application/json' opts.body = JSON.stringify(body) } if (args['dry-run']) { - const dryRunUrl = url.replace(TOKEN, '***') - return { _dry_run: true, method, url: dryRunUrl, headers: opts.headers, body: body || undefined } + return { _dry_run: true, method, url, headers: { ...opts.headers, Authorization: '***' }, body: body || undefined } } const res = await fetch(url, opts) const text = await res.text() diff --git a/tools/clis/trustpilot.js b/tools/clis/trustpilot.js index 66f58b4..0eb13d7 100755 --- a/tools/clis/trustpilot.js +++ b/tools/clis/trustpilot.js @@ -122,7 +122,7 @@ async function main() { case 'web-links': { if (!businessUnitId) { result = { error: '--business-unit or TRUSTPILOT_BUSINESS_UNIT_ID required' }; break } const locale = args.locale || 'en-US' - result = await api('GET', `/business-units/${businessUnitId}/web-links?locale=${locale}`) + result = await api('GET', `/business-units/${businessUnitId}/web-links?locale=${encodeURIComponent(locale)}`) break } default: @@ -134,10 +134,10 @@ async function main() { switch (sub) { case 'list': { if (!businessUnitId) { result = { error: '--business-unit or TRUSTPILOT_BUSINESS_UNIT_ID required' }; break } - const stars = args.stars ? `&stars=${args.stars}` : '' - const lang = args.language ? `&language=${args.language}` : '' - const orderBy = args['order-by'] || 'createdat.desc' - result = await api('GET', `/business-units/${businessUnitId}/reviews?perPage=${limit}&orderBy=${orderBy}${stars}${lang}`) + const reviewParams = new URLSearchParams({ perPage: String(limit), orderBy: args['order-by'] || 'createdat.desc' }) + if (args.stars) reviewParams.set('stars', args.stars) + if (args.language) reviewParams.set('language', args.language) + result = await api('GET', `/business-units/${businessUnitId}/reviews?${reviewParams}`) break } case 'get': { @@ -148,8 +148,9 @@ async function main() { } case 'private': { if (!businessUnitId) { result = { error: '--business-unit or TRUSTPILOT_BUSINESS_UNIT_ID required' }; break } - const stars = args.stars ? `&stars=${args.stars}` : '' - result = await api('GET', `/private/business-units/${businessUnitId}/reviews?perPage=${limit}${stars}`, null, 'bearer') + const privateParams = new URLSearchParams({ perPage: String(limit) }) + if (args.stars) privateParams.set('stars', args.stars) + result = await api('GET', `/private/business-units/${businessUnitId}/reviews?${privateParams}`, null, 'bearer') break } case 'latest': diff --git a/tools/clis/typeform.js b/tools/clis/typeform.js index 0606e73..c3a7cbe 100755 --- a/tools/clis/typeform.js +++ b/tools/clis/typeform.js @@ -128,7 +128,7 @@ async function main() { if (!id) { result = { error: '--id required (form ID)' }; break } const responseIds = args['response-ids'] if (!responseIds) { result = { error: '--response-ids required (comma-separated)' }; break } - result = await api('DELETE', `/forms/${id}/responses?included_response_ids=${responseIds}`) + result = await api('DELETE', `/forms/${id}/responses?included_response_ids=${encodeURIComponent(responseIds)}`) break } default: