Skip to content

Parameterize offline-storage deletes and harden kill-switch response handling#1467

Merged
bmehta001 merged 27 commits into
microsoft:mainfrom
bmehta001:bhamehta/offline-storage-parameterized-deletes
Jun 17, 2026
Merged

Parameterize offline-storage deletes and harden kill-switch response handling#1467
bmehta001 merged 27 commits into
microsoft:mainfrom
bmehta001:bhamehta/offline-storage-parameterized-deletes

Conversation

@bmehta001

@bmehta001 bmehta001 commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Builds OfflineStorage_SQLite::DeleteRecords as a parameterized prepared statement and tightens kill-switch response handling.

Changes

  • OfflineStorage_SQLite::DeleteRecords now builds a parameterized statement: column names are taken from a fixed whitelist and values are bound via sqlite3_bind_* on a single prepared statement instead of being concatenated into the SQL text. This is more robust and avoids quoting/escaping edge cases for values such as tenant tokens. Prepare/bind/execute results are checked and logged (with sqlite3_errmsg) on failure, and a non-numeric value for an integer filter column aborts the delete instead of coercing to 0.
  • DeleteRecords no longer issues a DELETE with an empty predicate, and fails closed (deletes nothing) on an unrecognized filter column.
  • KillSwitchManager::handleResponse rejects empty kill-token values, values containing control characters, and values longer than 256 bytes (kMaxTenantTokenBytes, a generous ceiling well above the ~74-char real token size) before storing them, while keeping opaque tokens — which may legitimately contain spaces/quotes — killable.
  • KillSwitchManager::handleResponse parses the Retry-After and kill-duration response headers through a non-throwing helper that accepts only an RFC 7231 1*DIGIT value surrounded by HTTP OWS (SP/HTAB) and clamps the result to a safe maximum, so a malformed value (the HTTP-date form of Retry-After, a leading sign, stray CR/LF, or an overflow-inducing magnitude) is ignored rather than propagating out of the SDK worker thread or wrapping the computed expiry time.
  • Adds OfflineStorageTests_SQLite regression tests for the parameterized DeleteRecords: deletion by tenant_token (including unusual characters and a SQL-injection attempt), deletion by a numeric column, multi-column AND filters, and fail-closed behavior on an empty filter, an invalid numeric value, and an unrecognized column.
  • Adds KillSwitchManagerTests covering valid input plus malformed Retry-After / kill-duration values (non-numeric, HTTP-date, out-of-range, trailing/leading garbage, leading sign, huge/clamped), control-character, empty, and opaque kill-tokens, and :all suffix stripping. These tests now also build and run on the Android and iOS native test targets.

Validation

  • KillSwitchManagerTests — 20 tests (the full new suite): valid Retry-After/kill-token input; malformed Retry-After/kill-duration (non-numeric, HTTP-date, out-of-range, trailing garbage, trailing/leading OWS, trailing CR/LF, leading sign, huge-clamped); control-character, empty, and opaque kill-tokens; and :all suffix stripping (including a token that strips to empty); and the tenant-token length cap (a 256-byte token stays killable, a 257-byte token is ignored). Now built/run on the host unit tests, Android, and iOS.
  • OfflineStorageTests_SQLite — 32 tests total, of which 7 exercise the parameterized DeleteRecords: SQL-injection attempt, delete-by-tenant_token (incl. unusual chars), delete-by-numeric-column, multi-column AND, empty filter, invalid numeric filter, and fail-closed on an unrecognized column.
  • The core parameterized-delete subset was validated locally on Windows x64 Debug; the full suites (including the additions above) run in CI across the host unit-test platforms, with the kill-switch tests additionally covered on Android and iOS.

Build OfflineStorage_SQLite::DeleteRecords as a parameterized prepared statement: column names come from a fixed whitelist and values are bound via sqlite3_bind_* on a single statement instead of being concatenated into the SQL text. This is more robust and avoids quoting/escaping edge cases for values such as tenant tokens. DeleteRecords also no longer issues a DELETE with an empty predicate.

Validate kill-token values against the expected tenant-token character set before storing them, and add OfflineStorageTests_SQLite regression tests covering deletion by tenant_token (including values with unusual characters).

Validated: full OfflineStorageTests_SQLite suite (27 tests) passes on Windows x64 Debug, including the new tests.

Files changed:

- lib/offline/OfflineStorage_SQLite.cpp

- lib/offline/KillSwitchManager.hpp

- tests/unittests/OfflineStorageTests_SQLite.cpp

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@bmehta001 bmehta001 requested a review from a team as a code owner June 8, 2026 07:01
The Retry-After and kill-duration response headers were parsed with
std::stoi without guarding against non-numeric or out-of-range input.
Because the SDK worker thread executes queued tasks without an exception
guard, an unparseable header value (for example the standards-compliant
HTTP-date form of Retry-After permitted by RFC 7231) could let the
exception escape the worker thread and terminate the process. Route both
parses through a non-throwing helper that ignores values it cannot parse.

Add KillSwitchManager unit tests covering valid input plus malformed
Retry-After / kill-duration values and a rejected malformed kill-token.

Files changed:
- lib/offline/KillSwitchManager.hpp
- tests/unittests/KillSwitchManagerTests.cpp (new)
- tests/unittests/CMakeLists.txt
- tests/unittests/UnitTests.vcxproj
- tests/unittests/UnitTests.vcxproj.filters

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens offline-storage deletion and kill-switch handling by switching OfflineStorage_SQLite::DeleteRecords to a parameterized SQLite prepared statement, avoiding concatenation of untrusted values into SQL and preventing accidental table-wide deletes.

Changes:

  • Reworks OfflineStorage_SQLite::DeleteRecords to use a whitelisted set of column names and sqlite3_bind_* value binding, and to bail out when no valid predicate is provided.
  • Makes KillSwitchManager::handleResponse resilient to malformed Retry-After / kill-duration values and rejects malformed kill-token tenant IDs.
  • Adds unit tests covering deletion by tenant_token (including SQL-injection-shaped input) and new KillSwitchManager header parsing behavior.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tests/unittests/UnitTests.vcxproj.filters Adds the new KillSwitchManager unit test source to VS project filters.
tests/unittests/UnitTests.vcxproj Adds the new KillSwitchManager unit test source to the unit test project build.
tests/unittests/OfflineStorageTests_SQLite.cpp Adds regression tests validating tenant-token deletes and SQL-injection-shaped input handling.
tests/unittests/KillSwitchManagerTests.cpp Introduces coverage for Retry-After, kill-token, and kill-duration parsing/validation.
tests/unittests/CMakeLists.txt Adds KillSwitchManager tests to the CMake unit test target sources.
lib/offline/OfflineStorage_SQLite.cpp Implements parameterized DELETE statement creation/binding and avoids empty-predicate deletes.
lib/offline/KillSwitchManager.hpp Adds safe duration parsing helper and tenant-token character validation before storing kill tokens.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/offline/OfflineStorage_SQLite.cpp Outdated
Comment thread lib/offline/OfflineStorage_SQLite.cpp
Comment thread lib/offline/OfflineStorage_SQLite.cpp Outdated
Comment thread lib/offline/KillSwitchManager.hpp
@bmehta001 bmehta001 changed the title Use parameterized queries for offline storage deletes Parameterize offline-storage deletes and harden kill-switch response handling Jun 8, 2026
bmehta001 and others added 3 commits June 8, 2026 09:48
Describe what the token validation and response-header parsing do without
spelling out threat-model detail. No behavior change.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Log the rejected column name when DeleteRecords ignores an
  unrecognized filter column, to aid diagnosing filter-key typos.
- Check sqlite3_bind_* return codes and abort the delete (logging
  sqlite3_errmsg) instead of executing with unbound parameters.
- Treat a non-numeric value for an integer filter column as an invalid
  filter and abort, rather than coercing to 0 and deleting rows that
  happen to match 0.
- Check the result of SqliteStatement::execute() and log on failure so
  a failed DELETE is not silent.
- KillSwitchManager::tryParseSeconds sets outSeconds=0 on parse failure
  so callers cannot read a stale value.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Self-review follow-up: MemoryStorage's matcher treats an unknown filter
column as "no match" and deletes nothing, but the SQLite path dropped the
unknown column and ran the remaining predicate. That could delete more
rows than intended and diverge from MemoryStorage even though
OfflineStorageHandler sends the same filter to both storages. Abort the
delete (remove nothing) when any filter column is outside the whitelist,
and add a regression test.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Comment thread lib/offline/KillSwitchManager.hpp
tryParseSeconds used std::stoll without checking how many characters were
consumed, so a numeric prefix followed by garbage (e.g. "120; foo") was
accepted as 120 - contradicting the "ignore malformed values" contract and
potentially activating Retry-After / kill-duration from a malformed header.
Validate full consumption (allowing only trailing whitespace) and add
regression tests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Comment thread lib/offline/KillSwitchManager.hpp Outdated
The header uses std::vector (handleResponse) and std::list (getTokensList)
but relied on transitive includes. Include them directly so the header is
self-contained and not sensitive to include order.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Comment thread lib/offline/KillSwitchManager.hpp Outdated
bmehta001 and others added 2 commits June 8, 2026 18:57
isValidTenantToken enforced an alnum + [-_.] allowlist, which would silently
drop legitimate kill-tokens: tenant tokens are opaque elsewhere in the SDK
(they may contain spaces/quotes) and the offline-storage DELETE is already
parameterized, so any value is safe to act on. Over-restricting meant a
stored tenant token could never be killed if the collector returned it
verbatim.

Relax the validator to reject only genuinely unsafe content -- control
characters (including CR/LF, to avoid log injection) and over-long values --
while accepting any other byte. Update tests: a control-char token is
rejected; an opaque token with quotes/spaces remains killable.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Comment thread lib/offline/KillSwitchManager.hpp Outdated
bmehta001 and others added 2 commits June 9, 2026 11:39
Copilot (KillSwitchManager.hpp:192): tryParseSeconds tolerated any
std::isspace() whitespace (incl. CR/LF, form-feed, vertical-tab) after the
number, which is broader than HTTP OWS (SP / HTAB per RFC 7230). A value like
"120\r\n" was accepted as 120, contradicting the intent to reject malformed
header values.
  -> Replaced std::isspace with an explicit SP/HTAB check, so CR/LF and other
     control whitespace are treated as malformed and the header is ignored.
     Removed the now-unused <cctype> include. Added regression test
     handleResponse_RetryAfterWithTrailingCRLF_IsIgnored. The existing
     TrailingWhitespace test already used "120  " (spaces), so it still passes.
  Verified at lib/offline/KillSwitchManager.hpp:182-191; logic confirmed with an
  isolated compile (10/10 cases: "120  "->120, "120\t"->120, "120\r\n"->rejected).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@bmehta001 bmehta001 requested a review from Copilot June 9, 2026 16:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.

Comment thread lib/offline/KillSwitchManager.hpp
isValidTenantToken (lalitb): there is no authoritative max tenant-token
size, and capping at 256 could silently let an over-long-but-legitimate
tenant escape the kill. Remove the `token.size() > 256` clause (keeping
the empty + control-character checks) and update the comment accordingly.
  lib/offline/KillSwitchManager.hpp:249.

KillSwitchManager.hpp (Copilot): `token.erase(pos, token.length() - pos)`
is equivalent to the simpler 1-arg `token.erase(pos)` (erase from pos to
end).
  lib/offline/KillSwitchManager.hpp:68.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.

Comment thread lib/offline/KillSwitchManager.hpp
Comment thread lib/offline/KillSwitchManager.hpp Outdated
bmehta001 and others added 2 commits June 11, 2026 16:34
KillSwitchManager.hpp (Copilot): `std::move(it->second)` on a
const_iterator binds to the copy constructor (you can't move from a
const lvalue), so it copied while implying a move. Drop the std::move so
the copy of `it->second` into `token` is honest.

The companion Copilot note — the PR description claimed kill-tokens are
rejected when "over-long", which is stale after removing the 256-byte
cap — is addressed by updating the PR description (no code change).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ized-deletes' into bhamehta/offline-storage-parameterized-deletes

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.

Comment thread lib/offline/KillSwitchManager.hpp Outdated
Comment thread tests/unittests/KillSwitchManagerTests.cpp
…en test

KillSwitchManager.hpp (Copilot): the comment above std::stoll claimed
only std::out_of_range can occur, but substr() can also throw (e.g.
std::bad_alloc); the std::exception catch below handles either. Relaxed
the comment so it doesn't mislead future edits.

tests/unittests/KillSwitchManagerTests.cpp (Copilot): added
handleResponse_EmptyKillToken_IsRejected to lock in the fail-closed
rejection of empty kill-tokens -- both a directly-empty token and one
that reduces to empty after ":all" stripping.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.

… known)

Restores the kMaxTenantTokenBytes (256) ceiling in isValidTenantToken that was
dropped in 2b35a2b. The cap was removed only because an authoritative max
token size was unconfirmed at the time; real tenant tokens are a fixed, small
size (~74 chars, per HttpRequestEncoder.cpp), so 256 is a generous ceiling --
far above any legitimate token, so it cannot make a real tenant unkillable
(the concern lalitb raised), while bounding memory growth from a malicious or
oversized kill-token in an untrusted HTTP response header.

Uses a named constant and adds two boundary regression tests: a 256-byte token
is still blocked; a 257-byte token is ignored. All 20 KillSwitchManager tests
pass (Release x64, v145).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.

Comment thread lib/offline/KillSwitchManager.hpp
Comment thread tests/unittests/OfflineStorageTests_SQLite.cpp Outdated
…token

Copilot review: the DeleteRecords SQL-injection test comment called the value a
"kill-token", but the filter key under test is the offline-storage tenant_token
field (not an HTTP kill-tokens response header). Wording-only fix.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.

@bmehta001 bmehta001 requested a review from lalitb June 16, 2026 05:39
@bmehta001 bmehta001 merged commit 21aef58 into microsoft:main Jun 17, 2026
31 of 32 checks passed
@bmehta001 bmehta001 deleted the bhamehta/offline-storage-parameterized-deletes branch June 17, 2026 16:58
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.

4 participants