Skip to content

fix(storage): escape address filter values to prevent SQL injection#1349

Open
ariel-formance wants to merge 4 commits into
mainfrom
fix/address-filter-sql-injection
Open

fix(storage): escape address filter values to prevent SQL injection#1349
ariel-formance wants to merge 4 commits into
mainfrom
fix/address-filter-sql-injection

Conversation

@ariel-formance

Copy link
Copy Markdown
Contributor

Summary

filterAccountAddress was interpolating user-supplied address filter values directly into SQL string literals using fmt.Sprintf, with no escaping. This allowed SQL injection via single-quote characters in filter values passed to the accounts and volumes endpoints.

Confirmed via testing:

  • {"$match": {"address": "doesnt_exist' OR '1'='1"}} returned all accounts in the ledger instead of zero results

Blast radius (low): injection is constrained to the authenticated user's ledger scope (table-level isolation holds) and the Go PostgreSQL driver blocks multi-statement execution, so no writes are possible. The risk is an authenticated user reading all account data/metadata within their ledger, bypassing any address-based filtering.

Fix

Added two helpers:

  • escapeSQL — escapes ''' for safe embedding in SQL string literals
  • escapeJSONPath — additionally escapes \ and " for safe embedding in JSONPath double-quoted strings inside SQL literals

Applied to both injection points in filterAccountAddress:

  • Exact address path: address = '%s' → uses escapeSQL
  • Partial address / JSONPath path: $[i] == "%s" → uses escapeJSONPath

Test plan

  • {"$match": {"address": "doesnt_exist' OR '1'='1"}} now returns 0 results
  • Valid partial filters like {"$match": {"address": ":foo"}} still work correctly
  • Existing test suite passes

filterAccountAddress was interpolating user-supplied values directly into
SQL string literals via fmt.Sprintf, allowing single-quote injection in
the exact-address path and single-quote/double-quote injection in the
JSONPath path.

Add escapeSQL ('' for single quotes) and escapeJSONPath (additionally
escapes backslashes and double quotes) helpers and apply them before
embedding values in the generated SQL fragments.
@ariel-formance ariel-formance requested a review from a team as a code owner May 15, 2026 15:37
@coderabbitai

coderabbitai Bot commented May 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9e15e989-6232-410c-80db-f393cc92f69e

📥 Commits

Reviewing files that changed from the base of the PR and between 51ab3db and 02361cf.

📒 Files selected for processing (1)
  • internal/storage/ledger/utils_test.go

Walkthrough

Adds SQL and JSONPath escaping helpers and applies them to address-filter SQL construction and transaction-level predicates; includes unit tests for normal and injection-like inputs.

Changes

SQL/JSONPath Escaping for Address Filtering

Layer / File(s) Summary
Escape helper functions
internal/storage/ledger/utils.go
Introduces escapeSQL and escapeJSONPath helpers to safely escape values for embedding in SQL literals and JSONPath expressions within SQL queries.
Address filter safety
internal/storage/ledger/utils.go
Updates filterAccountAddress to use escapeJSONPath for partial-address JSONPath segments and escapeSQL for exact-address values.
Transactions address filter usage
internal/storage/ledger/transactions.go
Escapes marshaled JSON payloads before interpolating into sources_arrays, destinations_arrays, sources, and destinations SQL predicates used for transaction address filtering.
Unit tests for escaping and filters
internal/storage/ledger/utils_test.go
Adds tests covering escapeSQL, escapeJSONPath, filterAccountAddress, and filterAccountAddressOnTransactions for normal cases and SQL-injection-like inputs.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

I nibble through quotes with flair and care,
I tuck each backslash in its lair,
Paths and strings now neat and sound,
No sneaky injections lurking round,
🐇✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately captures the main purpose: adding SQL escaping to prevent SQL injection in address filter values.
Description check ✅ Passed The description is directly related to the changeset, clearly explaining the SQL injection vulnerability, the fix approach, and the test plan.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/address-filter-sql-injection

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov

codecov Bot commented May 15, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 80.98%. Comparing base (9ebaa2a) to head (02361cf).
⚠️ Report is 4 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1349      +/-   ##
==========================================
+ Coverage   80.14%   80.98%   +0.84%     
==========================================
  Files         205      205              
  Lines       11106    11098       -8     
==========================================
+ Hits         8901     8988      +87     
+ Misses       1587     1566      -21     
+ Partials      618      544      -74     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai coderabbitai Bot 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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/storage/ledger/utils.go (1)

41-61: ⚠️ Potential issue | 🔴 Critical

SQL injection vulnerability found in filterAccountAddressOnTransactions in transactions.go — requires fix.

The verification revealed a critical vulnerability: filterAccountAddressOnTransactions (transactions.go, lines 272-321) uses the same unsafe pattern as the vulnerable code in filterAccountAddress. The function marshals user-controlled address input to JSON and directly interpolates it into SQL:

data, err := json.Marshal([]string{address})
parts = append(parts, fmt.Sprintf("sources @> '%s'", string(data)))

Since json.Marshal does not escape single quotes, an address containing a single quote (e.g., account:it's) produces malformed SQL: sources @> '["account:it's"]', allowing SQL injection.

This function is called from resource_transactions.go with user-controlled input from filter operators. Fix this using the same escaping approach applied to filterAccountAddress: properly escape or parameterize the JSON data before embedding in SQL.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/storage/ledger/utils.go` around lines 41 - 61,
filterAccountAddressOnTransactions in transactions.go constructs SQL by
embedding json.Marshal output directly, allowing SQL injection; update it to
either parameterize the query or escape the marshaled JSON the same way
filterAccountAddress does (use the existing escapeSQL helper) before
interpolating into the SQL string. Locate filterAccountAddressOnTransactions and
change the line that does parts = append(parts, fmt.Sprintf("sources @> '%s'",
string(data))) to use the escaped JSON (or a query parameter) so single quotes
are handled safely; also review the call site in resource_transactions.go to
ensure user-controlled address is passed through the fixed function.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@internal/storage/ledger/utils.go`:
- Around line 41-61: filterAccountAddressOnTransactions in transactions.go
constructs SQL by embedding json.Marshal output directly, allowing SQL
injection; update it to either parameterize the query or escape the marshaled
JSON the same way filterAccountAddress does (use the existing escapeSQL helper)
before interpolating into the SQL string. Locate
filterAccountAddressOnTransactions and change the line that does parts =
append(parts, fmt.Sprintf("sources @> '%s'", string(data))) to use the escaped
JSON (or a query parameter) so single quotes are handled safely; also review the
call site in resource_transactions.go to ensure user-controlled address is
passed through the fixed function.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 56f3fe64-9138-460d-bbe3-a42e8303f415

📥 Commits

Reviewing files that changed from the base of the PR and between 80b83ab and bdc5dc8.

📒 Files selected for processing (1)
  • internal/storage/ledger/utils.go

… address filters

json.Marshal escapes double-quotes but not single-quotes, so injecting a single
quote into a source/destination address filter broke out of the SQL string literal.
Apply the same escapeSQL() used in filterAccountAddress().
…filters

Covers escapeSQL, escapeJSONPath, filterAccountAddress, and
filterAccountAddressOnTransactions with single-quote payloads, double-quote
and backslash in JSONPath segments, and classic injection patterns.
Ensures valid addresses (exact, partial with colon, wildcard suffix, multi-key)
continue to produce correct SQL fragments after the escaping changes.
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.

2 participants