Skip to content

fix: send the parameters these endpoints actually require, and stop building JSON by hand - #11

Merged
franciscbalint merged 1 commit into
mainfrom
fix/s3-ipt-parameters-and-json-escaping
Oct 25, 2026
Merged

franciscbalint merged 1 commit into
mainfrom
fix/s3-ipt-parameters-and-json-escaping

Conversation

@franciscbalint

Copy link
Copy Markdown
Collaborator

Audited the S3/SFTP file jobs and the Inbox Placement Tester against the documented operations and the live API. Several calls could not succeed as the SDK built them, and the Tester built its request bodies in a way a quote in a caller's string could break.

Memory safety

  • The Tester's JSON bodies were concatenated into a fixed 1024-byte buffer with no escaping. A quote or a backslash in name, senderEmail or webhookUrl closed the string early and injected whatever followed, and anything longer than the buffer was silently truncated mid-field. Every value is escaped now and the bodies grow to fit (set_inbox_placement_webhook included).
  • send_file dereferenced SendFileOptions.return_url without a NULL check on the live request path, although the same value is checked where the request is noted. A caller that zeroed the struct instead of calling new_send_file_options crashed.

Parameters

Call Before Now
validate_s3_file return_url sent only when non-empty, never required; sent the non-spec remove_duplicate and allow_phase_2 return_url required; routed through the same helper as the other jobs
score_s3_file ScoreS3FileOptions had no return_url field, so the job could never succeed return_url field added and required
export_s3_file rejected a missing export_bucket as an empty parameter optional, as on the SFTP twin
send_email_finder_file no name column enforced one of first_name_column / full_name_column required
send_list_evaluator_file built its URL from bulk_api_base_url, ignoring list_evaluator_base_url honours the override, so both halves stay on one host
create_inbox_placement_test no providerIds, seedIds or providers[{id, maxSeeds}]; no limits all three added; name ≤ 64, testType 1|2|3, senderEmail required when testType is 1
list_inbox_placement_tests sent pageSize, which the API silently ignores; no date filters sends page_size (1–100) and the documented createdAfter / createdBefore

LIVE-verified: POST /validateS3file without a return_url answers 400 {"error_message":"Provided return URL is not permitted: None"}, and page_size is honoured where pageSize is silently ignored.

Also

  • Added zero_bounce_set_bulk_api_base_url, zero_bounce_set_bulk_api_root_url and zero_bounce_set_bulk_api_scoring_base_url. The regional-host warning in zb_check_request_url already told users to call the first one, but it did not exist anywhere in the codebase and the three bulk URLs were hardcoded to production.
  • Corrected the header comment claiming the Tester's key goes "on the query string" — the code has always used Authorization: Bearer, and the comment was telling readers to leak a key — plus two comments calling validate_sftp_file's file_name optional where the code requires it.

Compatibility

ScoreS3FileOptions gains a field and validate_s3_file / score_s3_file now reject a call without return_url, which the API rejected anyway. ValidateS3FileOptions.remove_duplicate, allow_phase_2 and allow_phase_2_is_set are kept so existing code still compiles, and are documented as no longer sent.

Tests

Offline Unity tests with mocked HTTP for every changed call: method, host and path, the full parameter set, where the key travels, and the error bodies ("success": false on 200, an HTML block page, an empty body, a detail error). No key is written anywhere.

99 Tests 0 Failures 0 Ignored
OK

cmake --build build is clean, a -Wall -Wextra -Wshadow build adds no warning that was not already there, the opt-in live suite still compiles, and the suite also passes under ASan + UBSan.

🤖 Generated with Claude Code

…uilding JSON by hand

Audited the S3/SFTP file jobs and the Inbox Placement Tester against the
documented 74 operations and the live API. Several calls could not succeed as
the SDK built them, and the Tester built its request bodies in a way that a
quote in a caller's string could break.

Memory safety first:

- The Tester's JSON bodies were concatenated into a fixed 1024-byte buffer with
  no escaping, so a quote or a backslash in `name`, `senderEmail` or
  `webhookUrl` closed the string early and injected whatever followed, and
  anything longer than the buffer was silently truncated mid-field. Every value
  is escaped now and the bodies grow to fit.
- `send_file` dereferenced `SendFileOptions.return_url` without a NULL check on
  the live request path, although the same value is checked where the request
  is noted. A caller that zeroed the struct rather than calling
  `new_send_file_options` crashed. Guarded.

Parameters:

- `validate_s3_file` and `score_s3_file` now require `return_url`, as the other
  six jobs already did: POST /validateS3file without it answers 400 "Provided
  return URL is not permitted: None". `ScoreS3FileOptions` had no `return_url`
  field at all, so that job could never succeed. Both now go through the same
  helper as the other jobs, which is also why `validate_s3_file` stops sending
  `remove_duplicate` and `allow_phase_2`: the endpoint documents
  `remove_duplicates`, and `allow_phase_2` belongs to the multipart bulk
  `send_file`.
- `export_s3_file` rejected a missing `export_bucket` as an empty parameter
  although the docs make it optional, and its SFTP twin treats it as optional.
  It is optional here too now.
- `send_email_finder_file` enforces the documented "first_name_column or
  full_name_column": without one the endpoint has no name to search for.
- `send_list_evaluator_file` built its URL from `bulk_api_base_url` while the
  result call used `list_evaluator_base_url`, so
  `zero_bounce_set_list_evaluator_base_url` split the two halves of the same
  feature across different hosts. It honours the override now.
- `list_inbox_placement_tests` sent `pageSize`, which the API accepts and then
  silently ignores, so a caller asking for a page size quietly got the default
  page. It sends `page_size`, bounded to 1-100, and gained the documented
  `createdAfter` / `createdBefore` filters.
- `create_inbox_placement_test` gained the unsupported `providerIds`, `seedIds`
  and `providers[{id, maxSeeds}]`, and enforces name <= 64, testType 1|2|3 and
  senderEmail when testType is 1.

Also: the regional-host warning told users to call
`zero_bounce_set_bulk_api_base_url`, which did not exist anywhere in the
codebase and left the three bulk URLs hardcoded to production. Added it along
with the root and scoring setters. Corrected the header comment claiming the
Tester's key goes on the query string; the code has always used
`Authorization: Bearer`, and the comment was telling readers to leak a key.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@franciscbalint
franciscbalint merged commit 76337bc into main Oct 25, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant