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
315 changes: 314 additions & 1 deletion src/session.c
Original file line number Diff line number Diff line change
Expand Up @@ -1966,6 +1966,61 @@ nc_session_seen_uris_free(char **seen_uris, int seen_count)
free(seen_uris);
}

/**
* @brief Restrict allowed CURL protocols to prevent SSRF via malicious CRL DP URIs.
*
* Only HTTP(S) and, if supported by the linked libcurl, LDAP(S) are allowed.
*
* @param[in] handle CURL handle to configure.
* @return 0 on success, 1 on failure.
*/
static int
nc_session_curl_set_protocols(CURL *handle)
{
curl_version_info_data *ver;
int i, have_ldap = 0, have_ldaps = 0;

#if CURL_AT_LEAST_VERSION(7, 85, 0)
char proto_str[32]; /* "http,https,ldap,ldaps" + NUL */
#else
long protocols = CURLPROTO_HTTP | CURLPROTO_HTTPS;
#endif

ver = curl_version_info(CURLVERSION_NOW);
if (ver && ver->protocols) {
for (i = 0; ver->protocols[i]; i++) {
if (!strcmp(ver->protocols[i], "ldap")) {
have_ldap = 1;
} else if (!strcmp(ver->protocols[i], "ldaps")) {
have_ldaps = 1;
}
}
}

#if CURL_AT_LEAST_VERSION(7, 85, 0)
/* curl 7.85.0+ uses a string-based option to avoid deprecation */
snprintf(proto_str, sizeof proto_str, "http,https%s%s",
have_ldap ? ",ldap" : "", have_ldaps ? ",ldaps" : "");
if (curl_easy_setopt(handle, CURLOPT_PROTOCOLS_STR, proto_str)) {
ERR(NULL, "Setting CURLOPT_PROTOCOLS_STR failed.");
return 1;
}
#else
if (have_ldap) {
protocols |= CURLPROTO_LDAP;
}
if (have_ldaps) {
protocols |= CURLPROTO_LDAPS;
}
if (curl_easy_setopt(handle, CURLOPT_PROTOCOLS, protocols)) {
ERR(NULL, "Setting CURLOPT_PROTOCOLS failed.");
return 1;
}
#endif

return 0;
}

/**
* @brief Initialize CURL handle for downloading CRL.
*
Expand Down Expand Up @@ -1998,6 +2053,11 @@ nc_session_curl_init(CURL **handle, struct nc_curl_data *data)
return 1;
}

/* restrict allowed protocols to prevent SSRF via malicious CRL DP URIs */
if (nc_session_curl_set_protocols(*handle)) {
return 1;
}

if (curl_easy_setopt(*handle, CURLOPT_WRITEFUNCTION, nc_session_curl_cb)) {
ERR(NULL, "Setting curl callback failed.");
return 1;
Expand All @@ -2011,6 +2071,252 @@ nc_session_curl_init(CURL **handle, struct nc_curl_data *data)
return 0;
}

/**
* @brief Advance past the end of the current LDIF line.
*
* @param[in] line_end Pointer to the end of the line content (after stripping CR).
* @param[in] data_end End of the whole LDIF buffer.
* @return Pointer to the start of the next line, or data_end if at the end.
*/
static char *
nc_session_ldif_next_line(char *line_end, char *data_end)
{
char *next;

if (line_end >= data_end) {
return data_end;
}

/* skip the CR of a CRLF pair */
if (*line_end == '\r') {
next = line_end + 2;
} else {
next = line_end + 1;
}

if (next > data_end) {
next = data_end;
}

return next;
}

/**
* @brief Check if an LDIF attribute name is a known CRL attribute.
*
* Accepts certificateRevocationList, authorityRevocationList, and
* deltaRevocationList, optionally followed by a transfer option such
* as ';binary' (RFC 4523).
*
* @param[in] name Attribute name (not null-terminated).
* @param[in] len Length of the attribute name.
* @return 1 if it is a CRL attribute, 0 otherwise.
*/
static int
nc_session_ldif_is_crl_attr(const char *name, uint32_t len)
{
static const char *crl_attrs[] = {"certificateRevocationList", "authorityRevocationList", "deltaRevocationList"};
uint32_t i, attr_len;

for (i = 0; i < (sizeof crl_attrs) / (sizeof crl_attrs[0]); i++) {
attr_len = (uint32_t)strlen(crl_attrs[i]);
/* the name must match exactly or be followed by ';' (transfer option) */
if ((len == attr_len) || ((len > attr_len) && (name[attr_len] == ';'))) {
if (!strncasecmp(name, crl_attrs[i], attr_len)) {
return 1;
}
}
}

return 0;
}

/**
* @brief Extract a binary CRL from an LDIF response.
*
* The LDIF data is not null-terminated, so we scan it as a byte buffer.
* We look for an attribute line carrying a known CRL attribute (see
* nc_session_ldif_is_crl_attr) with the '::' binary value marker, collect
* its base64 value (including folded continuation lines starting with a
* space), and decode it to raw DER.
*
* @param[in] ldif LDIF response data.
* @param[out] crl_data Extracted raw CRL data.
* @param[out] crl_size Size of the extracted CRL data.
* @return 0 on success, 1 on failure.
*/
static int
nc_session_ldif_extract_crl(struct nc_curl_data *ldif, unsigned char **crl_data, uint32_t *crl_size)
{
int ret, rc = 0;
char *line_start, *line_end, *b64_start, *attr_colon, *tmp;
char *b64_buf = NULL;
uint32_t b64_len = 0, b64_cap = 0, line_len;
char *pos, *data_end;

*crl_data = NULL;
*crl_size = 0;

if (!ldif->data || !ldif->size) {
return 1;
}

data_end = (char *)ldif->data + ldif->size;
pos = (char *)ldif->data;

while (pos < data_end) {
/* find end of current line */
line_start = pos;
line_end = memchr(pos, '\n', data_end - pos);
if (!line_end) {
line_end = data_end;
} else {
/* strip trailing \r */
if ((line_end > line_start) && (line_end[-1] == '\r')) {
line_end--;
}
}
line_len = (uint32_t)(line_end - line_start);

/* strip leading tabs (curl's LDIF output indents attribute lines) */
while ((line_len > 0) && (line_start[0] == '\t')) {
line_start++;
line_len--;
}

/* advance pos past the newline for the next iteration */
pos = nc_session_ldif_next_line(line_end, data_end);

/* skip empty lines and comments */
if ((line_len == 0) || (line_start[0] == '#')) {
continue;
}

/* skip continuation lines (start with space) */
if (line_start[0] == ' ') {
continue;
}

/* look for '::' which marks a binary attribute value */
attr_colon = memchr(line_start, ':', line_len);
if (attr_colon && (attr_colon + 1 < line_end) && (attr_colon[1] == ':')) {
/* check the attribute name is a known CRL attribute */
if (!nc_session_ldif_is_crl_attr(line_start, (uint32_t)(attr_colon - line_start))) {
continue;
}

/* found the CRL attribute; b64 starts after ":: " */
b64_start = attr_colon + 2;
if ((b64_start < line_end) && (b64_start[0] == ' ')) {
b64_start++;
}

/* collect this first chunk */
b64_len = (uint32_t)(line_end - b64_start);
b64_cap = b64_len + 1;
b64_buf = malloc(b64_cap);
NC_CHECK_ERRMEM_GOTO(!b64_buf, rc = 1, cleanup);
memcpy(b64_buf, b64_start, b64_len);
b64_buf[b64_len] = '\0';

/* now collect continuation lines (folded, start with space) */
while (pos < data_end) {
if ((pos[0] != ' ') && (pos[0] != '\t')) {
break;
}

line_start = pos;
line_end = memchr(pos, '\n', data_end - pos);
if (!line_end) {
line_end = data_end;
} else {
if ((line_end > line_start) && (line_end[-1] == '\r')) {
line_end--;
}
}
line_len = (uint32_t)(line_end - line_start);

/* advance pos */
pos = nc_session_ldif_next_line(line_end, data_end);

/* skip leading whitespace (the fold marker) */
while ((line_len > 0) && ((line_start[0] == ' ') || (line_start[0] == '\t'))) {
line_start++;
line_len--;
}

if (b64_len + line_len + 1 > b64_cap) {
b64_cap = b64_len + line_len + 1;
tmp = realloc(b64_buf, b64_cap);
NC_CHECK_ERRMEM_GOTO(!tmp, rc = 1, cleanup);
b64_buf = tmp;
}
memcpy(b64_buf + b64_len, line_start, line_len);
b64_len += line_len;
b64_buf[b64_len] = '\0';
}

break;
}
}

if (!b64_buf) {
ERR(NULL, "No binary CRL attribute found in LDIF response.");
rc = 1;
goto cleanup;
}

/* decode base64 to raw DER */
ret = nc_base64_decode_wrap(b64_buf, crl_data);
if (ret == -1) {
ERR(NULL, "Decoding base64 CRL data from LDIF failed.");
rc = 1;
goto cleanup;
}

/* nc_base64_decode_wrap returns the decoded length */
*crl_size = (uint32_t)ret;

cleanup:
free(b64_buf);
return rc;
}

/**
* @brief Fetch a CRL from an LDAP URL via CURL and parse the LDIF response.
*
* @param[in] handle CURL handle.
* @param[in] url LDAP URL to fetch the CRL from.
* @param[in,out] data Stores the downloaded LDIF, then the decoded raw CRL.
* @return 0 on success, 1 on failure.
*/
static int
nc_session_ldap_fetch(CURL *handle, const char *url, struct nc_curl_data *data)
{
int ret;
unsigned char *crl_data = NULL;
uint32_t crl_size = 0;

/* fetch via curl, which handles the LDAP protocol natively */
ret = nc_session_curl_fetch(handle, url);
if (ret) {
return ret;
}

/* parse the LDIF response and extract the base64-encoded CRL */
ret = nc_session_ldif_extract_crl(data, &crl_data, &crl_size);
if (ret) {
return ret;
}

/* replace the LDIF data with the decoded raw CRL */
free(data->data);
data->data = crl_data;
data->size = crl_size;

return 0;
}

/**
* @brief Download CRLs for certificates in the peer's chain and merge them into crl_store.
*
Expand Down Expand Up @@ -2066,9 +2372,16 @@ nc_session_tls_crl_fetch_for_peer_chain(void *peer_chain, void *cert_store, void
}

VRB(NULL, "Downloading CRL from \"%s\".", uris[j]);
ret = nc_session_curl_fetch(handle, uris[j]);
if (!strncasecmp(uris[j], "ldap://", 7) || !strncasecmp(uris[j], "ldaps://", 8)) {
ret = nc_session_ldap_fetch(handle, uris[j], &downloaded);
} else {
ret = nc_session_curl_fetch(handle, uris[j]);
}
if (ret) {
WRN(NULL, "Failed to fetch CRL from \"%s\".", uris[j]);
free(downloaded.data);
downloaded.data = NULL;
downloaded.size = 0;
continue;
}

Expand Down
23 changes: 19 additions & 4 deletions src/session_openssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -1114,18 +1114,33 @@ nc_tls_is_der_subpubkey_wrap(unsigned char *der, long len)
int
nc_base64_decode_wrap(const char *base64, unsigned char **bin)
{
int ret;
int ret, pad = 0;
uint32_t len;

len = (uint32_t)strlen(base64);

*bin = malloc((strlen(base64) / 4) * 3);
*bin = malloc((len / 4) * 3);
NC_CHECK_ERRMEM_RET(!*bin, -1);

ret = EVP_DecodeBlock(*bin, (const unsigned char *)base64, strlen(base64));
ret = EVP_DecodeBlock(*bin, (const unsigned char *)base64, len);
if (ret == -1) {
ERR(NULL, "Base64 decoding failed (%s).", ERR_reason_error_string(ERR_get_error()));
free(*bin);
*bin = NULL;
return -1;
}
return ret;

/* EVP_DecodeBlock returns a length rounded up to a multiple of 3 and does
* not strip base64 padding, so subtract trailing '=' chars for the real
* decoded length, matching mbedtls_base64_decode behaviour */
if ((len >= 1) && (base64[len - 1] == '=')) {
pad++;
}
if ((len >= 2) && (base64[len - 2] == '=')) {
pad++;
}

return ret - pad;
}

int
Expand Down