Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Security

- **SEC-012: Enforce explicit SSL certificate verification in embedding API requests** (#80)
- Added `CURLOPT_SSL_VERIFYPEER => true` and `CURLOPT_SSL_VERIFYHOST => 2` to all embedding HTTP requests
- Using `curl_setopt_array()` to ensure SSL options are always applied together
- Proxy configuration remains as a separate `curl_setopt()` call (unchanged)
- Added static analysis test `test_embedding_ssl_verify.phpt` to verify SSL options are present in source

### Added

- **TEST-001: ZVecException isolation tests for error code strings, constructor, chaining, and error details** (#99)
Expand Down
17 changes: 12 additions & 5 deletions src/embeddings/EmbeddingInterfaces.php
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,18 @@ protected function post(string $endpoint, array $data): array
$url = rtrim($this->baseUrl, '/') . '/' . ltrim($endpoint, '/');

$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout);
curl_setopt($ch, CURLOPT_HTTPHEADER, $this->getHeaders());
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($data),
CURLOPT_TIMEOUT => $this->timeout,
CURLOPT_HTTPHEADER => $this->getHeaders(),
// Explicit SSL verification — never trust silently even if global
// PHP curl options are changed (e.g., curl.cainfo="") or CA bundle
// is not configured. See SEC-012.
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
]);

if ($this->proxy !== null) {
curl_setopt($ch, CURLOPT_PROXY, $this->proxy);
Expand Down
54 changes: 54 additions & 0 deletions tests/test_embedding_ssl_verify.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php
/**
* SEC-012: Explicit SSL verification in embedding API requests.
*
* Verifies that CURLOPT_SSL_VERIFYPEER and CURLOPT_SSL_VERIFYHOST are
* explicitly set in the EmbeddingInterfaces.php source, using the
* curl_setopt_array() form (not individual curl_setopt() calls),
* and that the proxy option remains a separate curl_setopt() call.
*/

$source = file_get_contents(__DIR__ . '/../src/embeddings/EmbeddingInterfaces.php');

$hasSslVerifyPeer = str_contains($source, 'CURLOPT_SSL_VERIFYPEER');
$hasSslVerifyHost = str_contains($source, 'CURLOPT_SSL_VERIFYHOST');
$hasCurlSetoptArray = str_contains($source, 'curl_setopt_array');

echo "CURLOPT_SSL_VERIFYPEER present: " . ($hasSslVerifyPeer ? 'yes' : 'no') . "\n";
echo "CURLOPT_SSL_VERIFYHOST present: " . ($hasSslVerifyHost ? 'yes' : 'no') . "\n";
echo "curl_setopt_array used: " . ($hasCurlSetoptArray ? 'yes' : 'no') . "\n";

// Verify the values are set to secure defaults (curl_setopt_array format)
$peerMatch = preg_match('/CURLOPT_SSL_VERIFYPEER\s*=>\s*true/', $source);
$hostMatch = preg_match('/CURLOPT_SSL_VERIFYHOST\s*=>\s*2/', $source);

echo "SSL_VERIFYPEER set to true: " . ($peerMatch ? 'yes' : 'no') . "\n";
echo "SSL_VERIFYHOST set to 2: " . ($hostMatch ? 'yes' : 'no') . "\n";

// Verify options are in the post() method specifically (not just anywhere)
$postMethodStart = strpos($source, 'protected function post(');
$postMethodEnd = strpos($source, 'abstract protected function getHeaders(');
$postSource = substr($source, $postMethodStart, $postMethodEnd - $postMethodStart);

$postHasPeer = str_contains($postSource, 'CURLOPT_SSL_VERIFYPEER');
$postHasHost = str_contains($postSource, 'CURLOPT_SSL_VERIFYHOST');

echo "SSL options in post() method: " . ($postHasPeer && $postHasHost ? 'yes' : 'no') . "\n";

// Verify options are set before the proxy check (correct ordering)
$peerPos = strpos($postSource, 'CURLOPT_SSL_VERIFYPEER');
$proxyPos = strpos($postSource, 'CURLOPT_PROXY');
$correctOrder = $peerPos < $proxyPos;

echo "SSL options before proxy: " . ($correctOrder ? 'yes' : 'no') . "\n";

// Verify proxy uses separate curl_setopt (not in the array)
$proxyInArray = preg_match('/CURLOPT_PROXY\s*=>/', $postSource);
echo "Proxy NOT in curl_setopt_array: " . ($proxyInArray ? 'no' : 'yes') . "\n";

$allPass = $hasSslVerifyPeer && $hasSslVerifyHost && $hasCurlSetoptArray
&& $peerMatch && $hostMatch
&& $postHasPeer && $postHasHost
&& $correctOrder && !$proxyInArray;

echo ($allPass ? "PASS" : "FAIL") . "\n";
49 changes: 49 additions & 0 deletions tests/test_embedding_ssl_verify.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
--TEST--
SEC-012: Explicit SSL verification in embedding API requests
--FILE--
<?php
// Verify CURLOPT_SSL_VERIFYPEER and CURLOPT_SSL_VERIFYHOST are explicitly set
// in the EmbeddingInterfaces.php source. This test is a static analysis check —
// it does not make network requests or require FFI.
$source = file_get_contents(__DIR__ . '/../src/embeddings/EmbeddingInterfaces.php');

$hasVerifyPeer = strpos($source, 'CURLOPT_SSL_VERIFYPEER') !== false;
$hasVerifyHost = strpos($source, 'CURLOPT_SSL_VERIFYHOST') !== false;

if (!$hasVerifyPeer) {
echo "FAIL: CURLOPT_SSL_VERIFYPEER not found in EmbeddingInterfaces.php\n";
exit(1);
}
if (!$hasVerifyHost) {
echo "FAIL: CURLOPT_SSL_VERIFYHOST not found in EmbeddingInterfaces.php\n";
exit(1);
}

echo "PASS: CURLOPT_SSL_VERIFYPEER and CURLOPT_SSL_VERIFYHOST are explicitly set\n";

// Verify the values are true and 2 respectively
$patternPeer = '/CURLOPT_SSL_VERIFYPEER\s*=>\s*true/';
$patternHost = '/CURLOPT_SSL_VERIFYHOST\s*=>\s*2/';

if (!preg_match($patternPeer, $source)) {
echo "FAIL: CURLOPT_SSL_VERIFYPEER is not set to true\n";
exit(1);
}
if (!preg_match($patternHost, $source)) {
echo "FAIL: CURLOPT_SSL_VERIFYHOST is not set to 2\n";
exit(1);
}

echo "PASS: CURLOPT_SSL_VERIFYPEER => true and CURLOPT_SSL_VERIFYHOST => 2\n";

// Verify curl_setopt_array is used (not individual curl_setopt calls for SSL)
if (strpos($source, 'curl_setopt_array') === false) {
echo "FAIL: curl_setopt_array not used (should use array form for SSL options)\n";
exit(1);
}
echo "PASS: curl_setopt_array is used for setting curl options\n";
?>
--EXPECT--
PASS: CURLOPT_SSL_VERIFYPEER and CURLOPT_SSL_VERIFYHOST are explicitly set
PASS: CURLOPT_SSL_VERIFYPEER => true and CURLOPT_SSL_VERIFYHOST => 2
PASS: curl_setopt_array is used for setting curl options
Loading