Skip to content

Commit 513cb0e

Browse files
kiwicoppleclaudeburmecia
authored
hardening and docs (supabase#562)
* security(wasm_fdw): require fdw_package_checksum for supply chain protection Make fdw_package_checksum a required option for all WASM FDW servers. This ensures supply chain integrity by requiring verification of WASM package contents before execution. Co-Authored-By: Claude Opus 4.5 <[email protected]> * security(wrappers): implement credential masking to prevent leaks in error messages Add sanitize_error_message() utility to automatically mask sensitive values like API keys and tokens in error messages before they're displayed to users. - Add utils.rs with credential masking functions - Export sanitize_error_message in prelude - Add ProtectedOptionValue for tracking credentials in options - Apply credential masking to all HTTP-based FDWs: - Stripe, Airtable, Firebase, Logflare, Auth0, Cognito, DuckDB Co-Authored-By: Claude Opus 4.5 <[email protected]> * security(wrappers): add configurable response size limits to HTTP-based FDWs Add protection against DoS attacks via maliciously large HTTP responses. Each HTTP-based FDW now has a configurable max_response_size option (default: 10 MB) that rejects responses exceeding the limit. Affected FDWs: - Stripe: ResponseTooLarge error with size checking - Airtable: ResponseTooLarge error with size checking - Firebase: ResponseTooLarge error with size checking - Logflare: Changed from .json() to .text() for size checking - Auth0: ResponseTooLarge error type added - Cognito: ResponseTooLarge error type added Co-Authored-By: Claude Opus 4.5 <[email protected]> * docs(security): add platform-wide security documentation and tests Add comprehensive security documentation and tests for the Wrappers platform: - SECURITY.md: Platform-wide security documentation covering: - Supply chain security (WASM package verification) - Credential masking in error messages - Response size limits - wasm-wrappers/tests/test_wasm_security.sql: Platform-wide security tests - Supply chain verification tests - Credential masking tests Co-Authored-By: Claude Opus 4.5 <[email protected]> * docs: add .claude/CLAUDE.md for project context * fix(auth0,cognito): remove unused ResponseTooLarge error variants Auth0 and Cognito use different architectures (HTTP client / AWS SDK) that make response size checking impractical. Remove the unused error variants to avoid dead code. Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(utils): mask all occurrences of credentials in error messages The credential masking logic had two bugs: 1. The patterns array was created but never used - the code just did a simple string find 2. Only the first occurrence of each sensitive name was masked Now uses a while loop to find and mask ALL occurrences of each sensitive credential type in the message. Co-Authored-By: Claude Opus 4.5 <[email protected]> * docs(security): add Response Size Limits section to SECURITY.md Document the configurable max_response_size feature including: - Protection mechanism description - Configuration example with SQL - Default value (10 MB) - Supported FDWs table - Error behavior example - Implementation notes Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(lib): remove redundant credential masking exports from prelude The explicit re-exports of is_sensitive_option, mask_credential_value, mask_credentials_in_message, and sanitize_error_message are redundant since `pub use crate::utils::*` already exports all public items. Co-Authored-By: Claude Opus 4.5 <[email protected]> * style(options): fix formatting Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(tests): restructure credential masking test to avoid false positives The test now: 1. First checks all failure cases (any unmasked credential patterns) 2. Then determines the specific pass case with clearer messaging 3. Logs the actual error when credential wasn't in error path This avoids the ambiguous ELSE branch that could mask issues where the credential masking code wasn't being invoked at all. Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(utils): prevent infinite loops in credential masking Fixed several bugs in mask_credentials_in_message(): - Use position-based searching with search_start to track progress - Use lower_name.len() instead of sensitive_name.len() for correctness - Continue past unprocessable matches instead of breaking (which caused infinite loops when no '=' found, unclosed quotes, or empty values) Also improved Auth0 error sanitization comment to be more accurate. Co-Authored-By: Claude Opus 4.5 <[email protected]> * docs(security): document response size limit limitation The size check happens after the response is read into memory, which means extremely large responses could temporarily consume memory before being rejected. Document this limitation and suggest network-level controls for stronger protection. Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(instance): use struct variant syntax for OptionsError OptionsError::OptionValueIsInvalidUtf8 was changed to a struct variant with named field 'option_name' but instance.rs was still using tuple variant syntax. Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(utils): use strip_prefix instead of manual prefix stripping Clippy requires using strip_prefix() instead of starts_with() followed by manual slicing with [1..]. Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(options): add OptionParsingError variant to OptionsError The max_response_size parsing in FDWs uses OptionsError::OptionParsingError but the variant was missing from the enum definition. Co-Authored-By: Claude Opus 4.5 <[email protected]> * style(options): fix formatting Co-Authored-By: Claude Opus 4.5 <[email protected]> * docs(claude): add pre-commit checks to CLAUDE.md Add instructions to run cargo fmt, cargo check, and cargo clippy before committing to avoid CI failures. Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix(wasm_fdw): only require checksum for remote package URLs Local file:// URLs don't need supply chain protection since they reference locally-built WASM packages. Only require fdw_package_checksum for remote (non-file://) URLs. This fixes the wasm_smoketest which uses local file:// URLs. Co-Authored-By: Claude Opus 4.5 <[email protected]> * test(security): add unit tests for credential masking and options Add comprehensive unit tests to improve code coverage: - tests for mask_credential_value() - both long and short values - tests for is_sensitive_option() - various patterns, case insensitivity - tests for mask_credentials_in_message() - SQL, JSON, unquoted formats - tests for sanitize_error_message() - complex credential masking - tests for OptionsError variants - error message formatting - tests for require_option() and require_option_or() - tests for check_options_contain() Co-Authored-By: Claude Opus 4.5 <[email protected]> * chore: rename .claude folder to .agents Rename .claude/CLAUDE.md to .agents/README.md for better discoverability. Co-Authored-By: Claude Opus 4.5 <[email protected]> * ci(coverage): include supabase-wrappers unit tests in coverage Add cargo test for supabase-wrappers lib to include unit tests for credential masking and options handling in coverage measurement. Co-Authored-By: Claude Opus 4.5 <[email protected]> * format code * fix wrong AI generated code * Security: Improve credential masking implementation with UTF-8 safety and URL delimiter support - Fix UTF-8 handling in mask_credential_value() using char_indices for safe multi-byte character handling - Replace global replacen() with index-based string reconstruction to prevent wrong replacements when same value appears in multiple places - Add URL delimiters (&, #) to unquoted value termination set for proper URL parameter parsing - Add comprehensive tests for UTF-8 support and URL parameter masking - All 23 credential masking tests pass successfully Fixes credential leakage and masking correctness issues in error messages --------- Co-authored-by: Claude Opus 4.5 <[email protected]> Co-authored-by: Bo Lu <[email protected]> Co-authored-by: Bo Lu <[email protected]>
1 parent dd40651 commit 513cb0e

21 files changed

Lines changed: 1275 additions & 37 deletions

File tree

.agents/README.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# Wrappers
2+
3+
Postgres Foreign Data Wrapper (FDW) framework in Rust, by Supabase.
4+
5+
## Project Structure
6+
7+
```
8+
wrappers/ # Native FDWs (pgrx-based)
9+
src/fdw/ # Individual FDW implementations
10+
supabase-wrappers/ # Core FDW framework library
11+
supabase-wrappers-macros/ # Proc macros for FDW development
12+
wasm-wrappers/ # WASM-based FDWs (separate from workspace)
13+
fdw/ # Individual WASM FDW implementations
14+
wit/ # WebAssembly Interface Types
15+
```
16+
17+
## Build Commands
18+
19+
```bash
20+
# Native wrappers (requires pgrx)
21+
cargo build
22+
cargo pgrx install --pg-config [path] --features stripe_fdw
23+
24+
# WASM wrappers
25+
cd wasm-wrappers/fdw/<name>
26+
cargo component build --release
27+
```
28+
29+
## Before Committing
30+
31+
Always run these checks before committing to avoid CI failures:
32+
33+
```bash
34+
# Format check (required by CI)
35+
cargo fmt --check
36+
cargo fmt # to auto-fix
37+
38+
# Compile check (catches type errors, missing variants, etc.)
39+
cargo check
40+
41+
# Clippy (CI runs with -D warnings, so all warnings are errors)
42+
cargo clippy -- -D warnings
43+
```
44+
45+
If the local Rust version is too old for `cargo check`, at minimum run `cargo fmt` before pushing.
46+
47+
## Testing
48+
49+
- Native FDW tests: `cargo pgrx test`
50+
- WASM FDW tests: SQL files in `wasm-wrappers/fdw/<name>/tests/`
51+
52+
## Key Files
53+
54+
- `/SECURITY.md` - Platform-wide security documentation
55+
- `supabase-wrappers/src/utils.rs` - Shared utilities (credential masking)
56+
- `wrappers/src/fdw/wasm_fdw/` - WASM FDW host implementation
57+
58+
## Security Requirements
59+
60+
- All WASM FDWs require `fdw_package_checksum` option
61+
- Use `sanitize_error_message()` to mask credentials in errors
62+
- HTTP FDWs support `max_response_size` option (default 10MB)
63+
64+
## Adding a New Native FDW
65+
66+
1. Create module in `wrappers/src/fdw/<name>_fdw/`
67+
2. Implement `ForeignDataWrapper` trait
68+
3. Add feature flag to `wrappers/Cargo.toml`
69+
4. Use `sanitize_error_message()` for error handling
70+
71+
## Adding a New WASM FDW
72+
73+
1. Create directory `wasm-wrappers/fdw/<name>_fdw/`
74+
2. Implement the WIT interface from `wasm-wrappers/wit/`
75+
3. Add SQL tests in `tests/` subdirectory

.github/workflows/coverage.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ jobs:
5252
run: |
5353
source <(cargo llvm-cov show-env --export-prefix --no-cfg-coverage)
5454
cargo llvm-cov clean --workspace
55+
cargo test -p supabase-wrappers --lib
5556
cargo pgrx test --features "native_fdws" --manifest-path wrappers/Cargo.toml pg15
5657
cargo llvm-cov report --lcov --output-path lcov.info
5758

SECURITY.md

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
# Wrappers Platform Security
2+
3+
> **Security Disclosure**: If you discover a security vulnerability, please report it via https://supabase.com/.well-known/security.txt
4+
5+
This document covers security measures implemented across the entire Wrappers platform. For FDW-specific security concerns, see the individual FDW documentation.
6+
7+
---
8+
9+
## 1. Supply Chain Security - WASM Package Verification
10+
11+
### Protection
12+
WASM FDW servers with remote package URLs **require** the `fdw_package_checksum` option to prevent supply chain attacks. Local `file://` URLs are exempt since they reference locally-built packages.
13+
14+
```sql
15+
-- This will FAIL - checksum is required
16+
CREATE SERVER my_server
17+
FOREIGN DATA WRAPPER wasm_wrapper
18+
OPTIONS (
19+
fdw_package_url 'https://example.com/my_fdw.wasm',
20+
fdw_package_name 'my-fdw',
21+
fdw_package_version '1.0.0'
22+
-- Missing fdw_package_checksum!
23+
);
24+
25+
-- This will work
26+
CREATE SERVER my_server
27+
FOREIGN DATA WRAPPER wasm_wrapper
28+
OPTIONS (
29+
fdw_package_url 'https://example.com/my_fdw.wasm',
30+
fdw_package_name 'my-fdw',
31+
fdw_package_version '1.0.0',
32+
fdw_package_checksum 'sha256:abc123...' -- Required!
33+
);
34+
```
35+
36+
### Implementation
37+
- Enforced in `wrappers/src/fdw/wasm_fdw/wasm_fdw.rs` validator function
38+
- Checksum is verified when the WASM package is loaded
39+
- Mismatch causes the query to fail
40+
41+
### Calculating Checksums
42+
```bash
43+
sha256sum my_fdw.wasm | awk '{print "sha256:" $1}'
44+
```
45+
46+
---
47+
48+
## 2. Credential Masking in Error Messages
49+
50+
### Protection
51+
All FDW error messages are sanitized to prevent credential leakage using the `sanitize_error_message()` utility.
52+
53+
### Sensitive Patterns Detected
54+
The following option names are automatically masked:
55+
- Generic: `password`, `secret`, `token`, `api_key`, `credentials`
56+
- AWS: `aws_secret_access_key`, `aws_session_token`
57+
- GCP: `service_account_key`
58+
- Azure: `client_secret`, `storage_key`, `connection_string`
59+
- Database: `conn_string`, `db_password`
60+
- Service-specific: `stripe_api_key`, `firebase_credentials`, `motherduck_token`
61+
62+
### Example
63+
```
64+
Before: "Error: aws_secret_access_key = 'wJalrXUtnFEMI/K7MDENG' is invalid"
65+
After: "Error: aws_secret_access_key = 'wJal***' is invalid"
66+
```
67+
68+
### Implementation
69+
- Core utility in `supabase-wrappers/src/utils.rs`
70+
- Applied to error handlers in all FDWs:
71+
- DuckDB FDW (SQL with embedded credentials)
72+
- Airtable, Auth0, Firebase, Stripe, Logflare, Cognito FDWs
73+
74+
### Usage in Custom FDWs
75+
```rust
76+
use supabase_wrappers::prelude::sanitize_error_message;
77+
78+
impl From<MyFdwError> for ErrorReport {
79+
fn from(value: MyFdwError) -> Self {
80+
let error_message = sanitize_error_message(&format!("{value}"));
81+
ErrorReport::new(PgSqlErrorCode::ERRCODE_FDW_ERROR, error_message, "")
82+
}
83+
}
84+
```
85+
86+
---
87+
88+
## 3. Response Size Limits
89+
90+
### Protection
91+
HTTP-based FDWs enforce configurable response size limits to prevent denial-of-service attacks via maliciously large responses. Responses exceeding the limit are rejected before JSON parsing.
92+
93+
### Configuration
94+
Set `max_response_size` (in bytes) as a server option:
95+
96+
```sql
97+
CREATE SERVER stripe_server
98+
FOREIGN DATA WRAPPER stripe_wrapper
99+
OPTIONS (
100+
api_key_id 'your-vault-secret-id',
101+
max_response_size '5242880' -- 5 MB limit
102+
);
103+
```
104+
105+
### Default Value
106+
All supported FDWs default to **10 MB** (10,485,760 bytes) if not specified.
107+
108+
### Supported FDWs
109+
110+
| FDW | Support | Notes |
111+
|-----|---------|-------|
112+
| Stripe || Full implementation |
113+
| Airtable || Full implementation |
114+
| Firebase || Full implementation |
115+
| Logflare || Full implementation |
116+
| Auth0 || Uses SDK architecture |
117+
| Cognito || Uses AWS SDK |
118+
| DuckDB || Not HTTP-based |
119+
| WASM FDWs | Per-FDW | Implemented individually |
120+
121+
### Error Behavior
122+
When a response exceeds the limit:
123+
```
124+
ERROR: response too large (15728640 bytes). Maximum allowed: 10485760 bytes
125+
```
126+
127+
### Implementation
128+
- Each FDW has a `ResponseTooLarge` error variant
129+
- Response body is read as text and size-checked before JSON parsing
130+
- Example in `wrappers/src/fdw/stripe_fdw/stripe_fdw.rs`
131+
132+
### Known Limitations
133+
The size check occurs **after** the response body is read into memory. This means:
134+
- An extremely large response could temporarily consume memory before being rejected
135+
- This protects against storing/parsing oversized data, but not against memory exhaustion during the HTTP read phase
136+
- For stronger protection, consider network-level controls (e.g., reverse proxy response size limits)
137+
138+
Future improvement: Implement streaming reads with size checking during the read phase.
139+
140+
---
141+
142+
## 4. Option Value Security
143+
144+
### Protection
145+
Option values that fail UTF-8 validation do not include the raw bytes in error messages, preventing binary credential leakage.
146+
147+
### Implementation
148+
- `supabase-wrappers/src/options.rs` - `OptionsError::OptionValueIsInvalidUtf8` only includes option name, not value
149+
150+
---
151+
152+
## Security Status
153+
154+
| Protection | Status | Location |
155+
|------------|--------|----------|
156+
| WASM checksum required | ✓ Implemented | `wasm_fdw.rs` validator |
157+
| Credential masking | ✓ Implemented | `utils.rs`, all FDW error handlers |
158+
| Response size limits | ✓ Implemented | Stripe, Airtable, Firebase, Logflare FDWs |
159+
| Option value protection | ✓ Implemented | `options.rs` |
160+
161+
---
162+
163+
## FDW-Specific Security
164+
165+
Each FDW may have additional security considerations:
166+
167+
- **AWS FDW**: SSRF protection for `endpoint_url` - see `wasm-wrappers/fdw/aws_fdw/SECURITY.md`
168+
- **DuckDB FDW**: SQL injection via server type options
169+
- **HTTP-based FDWs**: Request/response validation
170+
171+
---
172+
173+
## Reporting Security Issues
174+
175+
Please report security vulnerabilities to the Supabase security team rather than opening public issues.

supabase-wrappers/src/instance.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,8 @@ pub(super) unsafe fn create_fdw_instance_from_server_id<
2828
let c_str = unsafe { CStr::from_ptr(raw) };
2929
let value = c_str
3030
.to_str()
31-
.map_err(|_| {
32-
OptionsError::OptionValueIsInvalidUtf8(
33-
String::from_utf8_lossy(c_str.to_bytes()).to_string(),
34-
)
31+
.map_err(|_| OptionsError::OptionValueIsInvalidUtf8 {
32+
option_name: String::from_utf8_lossy(c_str.to_bytes()).to_string(),
3533
})
3634
.report_unwrap()
3735
.to_string();

0 commit comments

Comments
 (0)