Skip to content

Commit 06ce88c

Browse files
hyperpolymathclaude
andcommitted
feat(zig-api): wire proven_header_has_crlf CRLF-injection guard into gnosis
Add safeHeaderDefault helper that calls proven_header_has_crlf (formally-verified from libproven_ffi) to validate response headers for CRLF injection before serialisation. Integrate check into writeResponse() — if Content-Type header contains CRLF or proven returns an error, refuse entire response with 500 error (fail-closed policy). Extern declarations for proven_header_has_crlf match proven.h C ABI. Comments document the proven → zig-api → gnosis chaining. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c33c2b6 commit 06ce88c

1 file changed

Lines changed: 60 additions & 0 deletions

File tree

zig-api/ffi/zig/src/gnosis.zig

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,33 @@ const std = @import("std");
2727
const core = @import("core.zig");
2828
const process = @import("process.zig");
2929

30+
// =============================================================================
31+
// proven FFI — extern declarations matching proven.h
32+
// =============================================================================
33+
34+
/// C ABI result type for boolean operations (matches ProvenBoolResult in proven.h).
35+
/// Layout must match: struct { int32_t status; bool value; }
36+
const ProvenBoolResult = extern struct {
37+
status: c_int,
38+
value: bool,
39+
};
40+
41+
/// proven status codes (subset used here).
42+
const PROVEN_OK: c_int = 0;
43+
44+
/// Check if a byte slice contains CRLF injection characters ("\r\n").
45+
/// Implemented in verification-ecosystem/proven/ffi/zig/src/main.zig.
46+
/// Linked via -lproven_ffi in build.zig.
47+
extern fn proven_header_has_crlf(ptr: [*]const u8, len: usize) ProvenBoolResult;
48+
49+
/// Error returned when proven_header_has_crlf detects CRLF injection.
50+
pub const HeaderError = error{
51+
/// proven detected CRLF injection characters in the header.
52+
CRLFInjectionDetected,
53+
/// proven returned an unexpected status code.
54+
ProvenStatusError,
55+
};
56+
3057
// =============================================================================
3158
// Constants
3259
// =============================================================================
@@ -193,12 +220,25 @@ fn idxFromHandle(handle: u64) ?usize {
193220

194221
/// Write a minimal HTTP/1.1 response with a text body.
195222
/// `status_code` e.g. 200, 400, 404, 503.
223+
///
224+
/// Validates Content-Type header using safeHeaderDefault() to prevent CRLF
225+
/// injection attacks. If validation fails, the response is refused entirely
226+
/// and a 500 error is sent instead (fail-closed policy).
196227
fn writeResponse(
197228
conn: *std.net.Server.Connection,
198229
status_code: u16,
199230
content_type: []const u8,
200231
body: []const u8,
201232
) void {
233+
// Validate Content-Type header against CRLF injection.
234+
// safeHeaderDefault returns false if CRLF is detected or proven errors.
235+
if (!safeHeaderDefault(content_type)) {
236+
// CRLF injection detected or proven validation failed.
237+
// Refuse the response entirely (fail-closed).
238+
writeInternalError(conn, "response header validation failed");
239+
return;
240+
}
241+
202242
// Reuse a stack buffer for the status line + headers.
203243
var header_buf: [512]u8 = undefined;
204244
const headers = std.fmt.bufPrint(
@@ -228,6 +268,26 @@ fn writeInternalError(conn: *std.net.Server.Connection, msg: []const u8) void {
228268
writeResponse(conn, 500, "application/json", body);
229269
}
230270

271+
/// Check if a header name or value is safe (no CRLF injection).
272+
/// Uses proven_header_has_crlf (formally-verified detection from libproven_ffi).
273+
/// Returns false if CRLF is detected or if proven returns an error.
274+
///
275+
/// This is a load-bearing proven FFI call: writeResponse uses this to validate
276+
/// headers before adding them to the response. If any header fails validation,
277+
/// the entire response is refused (fail-closed policy).
278+
fn safeHeaderDefault(value: []const u8) bool {
279+
if (value.len == 0) return true; // empty header is safe
280+
const result = proven_header_has_crlf(value.ptr, value.len);
281+
if (result.status != PROVEN_OK) {
282+
// proven returned an error: fail closed.
283+
core.setError("proven_header_has_crlf returned status {d} for header '{s}'",
284+
.{ result.status, value });
285+
return false;
286+
}
287+
// result.value == true means CRLF detected → deny.
288+
return !result.value;
289+
}
290+
231291
// =============================================================================
232292
// Request body reading
233293
// =============================================================================

0 commit comments

Comments
 (0)