Coverage Report

Created: 2026-09-27 07:07

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/mod_auth_openidc/src/state.c
Line
Count
Source
1
/*
2
 * Licensed to the Apache Software Foundation (ASF) under one
3
 * or more contributor license agreements.  See the NOTICE file
4
 * distributed with this work for additional information
5
 * regarding copyright ownership.  The ASF licenses this file
6
 * to you under the Apache License, Version 2.0 (the
7
 * "License"); you may not use this file except in compliance
8
 * with the License.  You may obtain a copy of the License at
9
 *
10
 *   http://www.apache.org/licenses/LICENSE-2.0
11
 *
12
 * Unless required by applicable law or agreed to in writing,
13
 * software distributed under the License is distributed on an
14
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15
 * KIND, either express or implied.  See the License for the
16
 * specific language governing permissions and limitations
17
 * under the License.
18
 */
19
20
/***************************************************************************
21
 * Copyright (C) 2017-2026 ZmartZone Holding BV
22
 * All rights reserved.
23
 *
24
 * DISCLAIMER OF WARRANTIES:
25
 *
26
 * THE SOFTWARE PROVIDED HEREUNDER IS PROVIDED ON AN "AS IS" BASIS, WITHOUT
27
 * ANY WARRANTIES OR REPRESENTATIONS EXPRESS, IMPLIED OR STATUTORY; INCLUDING,
28
 * WITHOUT LIMITATION, WARRANTIES OF QUALITY, PERFORMANCE, NONINFRINGEMENT,
29
 * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.  NOR ARE THERE ANY
30
 * WARRANTIES CREATED BY A COURSE OR DEALING, COURSE OF PERFORMANCE OR TRADE
31
 * USAGE.  FURTHERMORE, THERE ARE NO WARRANTIES THAT THE SOFTWARE WILL MEET
32
 * YOUR NEEDS OR BE FREE FROM ERRORS, OR THAT THE OPERATION OF THE SOFTWARE
33
 * WILL BE UNINTERRUPTED.  IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR
34
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
35
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES HOWEVER CAUSED AND ON ANY THEORY OF
36
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
37
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
38
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
39
 *
40
 * @Author: Hans Zandbelt - hans.zandbelt@openidc.com
41
 */
42
43
#include "state.h"
44
#include "cfg/dir.h"
45
#include "jose.h"
46
#include "proto/proto.h"
47
#include "util/util.h"
48
#include "util/util_cfg.h"
49
50
/*
51
 * return the name for the state cookie
52
 */
53
0
char *oidc_state_cookie_name(request_rec *r, const char *state) {
54
0
  return apr_psprintf(r->pool, "%s%s", oidc_cfg_dir_state_cookie_prefix_get(r), state);
55
0
}
56
57
/*
58
 * calculates a hash value based on request fingerprint plus a provided nonce string.
59
 */
60
1.74k
char *oidc_state_browser_fingerprint(request_rec *r, const oidc_cfg_t *c, const char *nonce) {
61
62
  /* helper to hold header values */
63
1.74k
  const char *value = NULL;
64
  /* concatenated hash input */
65
1.74k
  char *input = "";
66
1.74k
  unsigned char *hash = NULL;
67
1.74k
  unsigned int hash_len = 0;
68
1.74k
  oidc_jose_error_t err;
69
1.74k
  char *result = NULL;
70
71
1.74k
  oidc_debug(r, "enter");
72
73
1.74k
  if (oidc_cfg_state_input_headers_get(c) & OIDC_STATE_INPUT_HEADERS_X_FORWARDED_FOR) {
74
    /* get the X-FORWARDED-FOR header value  */
75
0
    value = oidc_http_hdr_in_x_forwarded_for_get(r);
76
    /* if we have a value for this header, concat it to the hash input */
77
0
    if (value != NULL)
78
0
      input = apr_pstrcat(r->pool, input, value, NULL);
79
0
  }
80
81
1.74k
  if (oidc_cfg_state_input_headers_get(c) & OIDC_STATE_INPUT_HEADERS_USER_AGENT) {
82
    /* get the USER-AGENT header value  */
83
1.74k
    value = oidc_http_hdr_in_user_agent_get(r);
84
    /* if we have a value for this header, concat it to the hash input */
85
1.74k
    if (value != NULL)
86
1.74k
      input = apr_pstrcat(r->pool, input, value, NULL);
87
1.74k
  }
88
89
  /* get the remote client IP address or host name */
90
91
  /* concat the nonce parameter to the hash input */
92
1.74k
  input = apr_pstrcat(r->pool, input, nonce, NULL);
93
94
  /* calculate the hash output */
95
1.74k
  if (oidc_jose_hash_bytes(r->pool, OIDC_JOSE_ALG_SHA256, (const unsigned char *)input,
96
1.74k
         (unsigned int)_oidc_strlen(input), &hash, &hash_len, &err) == FALSE) {
97
0
    oidc_error(r, "oidc_jose_hash_bytes failed: %s", oidc_jose_e2s(r->pool, err));
98
0
    return NULL;
99
0
  }
100
101
  /* base64url-encode the resulting hash and return it */
102
1.74k
  oidc_util_base64url_encode(r, &result, (const char *)hash, hash_len, OIDC_BASE64URL_PADDING_STRIP);
103
104
1.74k
  return result;
105
1.74k
}
106
107
// element in a list of state cookies
108
typedef struct oidc_state_cookies_t {
109
  char *name;
110
  apr_time_t timestamp;
111
  struct oidc_state_cookies_t *next;
112
} oidc_state_cookies_t;
113
114
/*
115
 * delete superfluous state cookies i.e. exceeding the maximum, starting with the oldest ones
116
 */
117
static int oidc_state_cookies_delete_oldest(request_rec *r, const oidc_cfg_t *c, int number_of_valid_state_cookies,
118
0
              int max_number_of_state_cookies, oidc_state_cookies_t *first) {
119
0
  oidc_state_cookies_t *cur = NULL;
120
0
  oidc_state_cookies_t *prev = NULL;
121
0
  oidc_state_cookies_t *prev_oldest = NULL;
122
0
  oidc_state_cookies_t *oldest = NULL;
123
  // loop over the list of state cookies, deleting the oldest one until we reach an acceptable number
124
0
  while (number_of_valid_state_cookies >= max_number_of_state_cookies) {
125
0
    oldest = first;
126
0
    prev_oldest = NULL;
127
0
    prev = first;
128
0
    cur = first ? first->next : NULL;
129
    // find the oldest state cookie in the list (stored in "oldest")
130
0
    while (cur) {
131
0
      if (cur->timestamp < oldest->timestamp) {
132
0
        oldest = cur;
133
0
        prev_oldest = prev;
134
0
      }
135
0
      prev = cur;
136
0
      cur = cur->next;
137
0
    }
138
0
    if (oldest) {
139
0
      oidc_warn(r, "deleting oldest state cookie: %s (time until expiry %" APR_TIME_T_FMT " seconds)",
140
0
          oldest->name, apr_time_sec(oldest->timestamp - apr_time_now()));
141
0
      oidc_http_set_cookie(r, oldest->name, "", 0, OIDC_HTTP_COOKIE_SAMESITE_NONE(c, r));
142
0
      if (prev_oldest)
143
0
        prev_oldest->next = oldest->next;
144
0
      else
145
0
        first = first->next;
146
0
    }
147
0
    number_of_valid_state_cookies--;
148
0
  }
149
0
  return number_of_valid_state_cookies;
150
0
}
151
152
/*
153
 * append a state cookie record to the tail of the linked list of still-valid cookies
154
 */
155
static void oidc_state_cookies_list_append(request_rec *r, oidc_state_cookies_t **first, oidc_state_cookies_t **last,
156
0
             char *name, apr_time_t ts) {
157
0
  if (*first == NULL) {
158
0
    *first = apr_pcalloc(r->pool, sizeof(oidc_state_cookies_t));
159
0
    *last = *first;
160
0
  } else {
161
0
    (*last)->next = apr_pcalloc(r->pool, sizeof(oidc_state_cookies_t));
162
0
    *last = (*last)->next;
163
0
  }
164
0
  (*last)->name = name;
165
0
  (*last)->timestamp = ts;
166
0
  (*last)->next = NULL;
167
0
}
168
169
/*
170
 * process a single state cookie: skip if it's the current one, delete if expired or undecodable,
171
 * otherwise append it to the list of valid cookies; returns 1 if the cookie was kept
172
 */
173
static int oidc_state_cookies_process_one(request_rec *r, const oidc_cfg_t *c, char *cookieName, const char *value,
174
            const char *currentCookieName, oidc_state_cookies_t **first,
175
0
            oidc_state_cookies_t **last) {
176
  /* never touch the cookie associated with the request currently being processed */
177
0
  if ((currentCookieName != NULL) && (_oidc_strcmp(cookieName, currentCookieName) == 0))
178
0
    return 0;
179
180
0
  oidc_proto_state_t *proto_state = oidc_proto_state_from_cookie(r, c, value);
181
0
  if (proto_state == NULL) {
182
0
    oidc_warn(r, "state cookie could not be retrieved/decoded, deleting: %s", cookieName);
183
0
    oidc_http_set_cookie(r, cookieName, "", 0, OIDC_HTTP_COOKIE_SAMESITE_NONE(c, r));
184
0
    return 0;
185
0
  }
186
187
0
  int kept = 0;
188
0
  oidc_json_int_t ts = oidc_proto_state_get_timestamp(proto_state);
189
0
  if (apr_time_now() > ts + apr_time_from_sec(oidc_cfg_state_timeout_get(c))) {
190
0
    oidc_warn(r, "state (%s) has expired (original_url=%s)", cookieName,
191
0
        oidc_proto_state_get_original_url(proto_state));
192
0
    oidc_http_set_cookie(r, cookieName, "", 0, OIDC_HTTP_COOKIE_SAMESITE_NONE(c, r));
193
0
  } else {
194
0
    oidc_state_cookies_list_append(r, first, last, cookieName, ts);
195
0
    kept = 1;
196
0
  }
197
198
0
  oidc_proto_state_destroy(proto_state);
199
0
  return kept;
200
0
}
201
202
/*
203
 * parse a single "<name>=<value>" cookie token and dispatch processing if it is a state cookie;
204
 * returns 1 if the token was kept as a valid state cookie
205
 */
206
static int oidc_state_cookies_parse_token(request_rec *r, const oidc_cfg_t *c, char *cookie,
207
            const char *currentCookieName, oidc_state_cookies_t **first,
208
0
            oidc_state_cookies_t **last) {
209
0
  while (*cookie == OIDC_CHAR_SPACE)
210
0
    cookie++;
211
212
0
  if (_oidc_strstr(cookie, oidc_cfg_dir_state_cookie_prefix_get(r)) != cookie)
213
0
    return 0;
214
215
0
  char *cookieName = cookie;
216
  /* stop at the string terminator as well as at '='; the previous "cookie != NULL" condition could
217
   * never be false (cookie is only incremented) so a state-prefixed token without a '=' would scan
218
   * past the end of the buffer (out-of-bounds read, and a subsequent out-of-bounds NUL write) */
219
0
  while ((*cookie != '\0') && (*cookie != OIDC_CHAR_EQUAL))
220
0
    cookie++;
221
0
  if (*cookie != OIDC_CHAR_EQUAL)
222
0
    return 0;
223
224
0
  *cookie = '\0';
225
0
  cookie++;
226
227
0
  return oidc_state_cookies_process_one(r, c, cookieName, cookie, currentCookieName, first, last);
228
0
}
229
230
/*
231
 * clean state cookies that have expired i.e. for outstanding requests that will never return
232
 * successfully and return the number of remaining valid cookies/outstanding-requests while
233
 * doing so
234
 */
235
int oidc_state_cookies_clean_expired(request_rec *r, const oidc_cfg_t *c, const char *currentCookieName,
236
0
             int delete_oldest) {
237
0
  int number_of_valid_state_cookies = 0;
238
0
  oidc_state_cookies_t *first = NULL;
239
0
  oidc_state_cookies_t *last = NULL;
240
0
  char *tokenizerCtx = NULL;
241
0
  char *cookies = apr_pstrdup(r->pool, oidc_http_hdr_in_cookie_get(r));
242
243
0
  if (cookies == NULL)
244
0
    goto out;
245
246
0
  char *cookie = apr_strtok(cookies, OIDC_STR_SEMI_COLON, &tokenizerCtx);
247
0
  while (cookie != NULL) {
248
0
    number_of_valid_state_cookies +=
249
0
        oidc_state_cookies_parse_token(r, c, cookie, currentCookieName, &first, &last);
250
0
    cookie = apr_strtok(NULL, OIDC_STR_SEMI_COLON, &tokenizerCtx);
251
0
  }
252
253
0
out:
254
255
0
  if (delete_oldest > 0)
256
0
    number_of_valid_state_cookies = oidc_state_cookies_delete_oldest(
257
0
        r, c, number_of_valid_state_cookies, oidc_cfg_max_number_of_state_cookies_get(c), first);
258
259
0
  return number_of_valid_state_cookies;
260
0
}