Nilorea Library
C utilities for networking, threading, graphics
Loading...
Searching...
No Matches
ex_http_cookie.c

n_http_cookie: reading a Cookie header (first occurrence wins) and building Set-Cookie values, with header injection and broken prefixes refused

n_http_cookie: reading a Cookie header (first occurrence wins) and building Set-Cookie values, with header injection and broken prefixes refused

Author
Castagnier Mickael
Version
1.0
Date
23/10/2026
/*
* Nilorea Library
* Copyright (C) 2005-2026 Castagnier Mickael
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
* implied. See the License for the specific language governing
* permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "nilorea/n_log.h"
#include "nilorea/n_str.h"
static int log_level = LOG_ERR;
static int failures = 0;
static void expect_true(const char* what, int ok) {
if (!ok) {
n_log(LOG_ERR, "FAIL: %s", what);
} else {
n_log(LOG_INFO, "ok: %s", what);
}
}
static int process_args(int argc, char** argv) {
int opt = 0;
while ((opt = getopt(argc, argv, "V:")) != -1) {
if (opt == 'V') {
log_level = atoi(optarg);
} else {
return -1;
}
}
return 0;
}
static int cookie_is(const char* header, const char* name, const char* want) {
size_t vl = 99;
const char* v = n_http_cookie_get(header, strlen(header), name, &vl);
if (!want)
return v == NULL && vl == 0;
return v && vl == strlen(want) && memcmp(v, want, vl) == 0;
}
static void test_read(void) {
const char* h = "a=first; b=x%41;a=second; c=\"q\"; bad name=1; =v; d; e = spaced ;;f=";
expect_true("first occurrence wins", cookie_is(h, "a", "first"));
expect_true("both occurrences counted", n_http_cookie_count(h, strlen(h), "a") == 2);
expect_true("value kept raw (percent escape)", cookie_is(h, "b", "x%41"));
expect_true("value kept raw (quotes)", cookie_is(h, "c", "\"q\""));
expect_true("pair with a non-token name skipped", cookie_is(h, "bad name", NULL) && cookie_is(h, "name", NULL));
expect_true("pair without '=' has an empty value", cookie_is(h, "d", ""));
expect_true("spaces around name and value trimmed", cookie_is(h, "e", "spaced"));
expect_true("empty value", cookie_is(h, "f", ""));
expect_true("absent cookie", cookie_is(h, "zz", NULL));
expect_true("names are case-sensitive", cookie_is(h, "A", NULL));
expect_true("a name prefix does not match", cookie_is("abc=1", "ab", NULL));
expect_true("__Host- name found", cookie_is("__Host-nsid=abc123; other=1", "__Host-nsid", "abc123"));
expect_true("empty header", cookie_is("", "a", NULL));
expect_true("NULL header", n_http_cookie_get(NULL, 0, "a", NULL) == NULL && n_http_cookie_count(NULL, 0, "a") == 0);
expect_true("empty name", n_http_cookie_get("a=1", 3, "", NULL) == NULL);
}
static int builds(const N_HTTP_SET_COOKIE* c, const char* want) {
N_STR* out = NULL;
int ok = n_http_set_cookie_build(&out, c);
int res = want ? (ok == TRUE && out && strcmp(out->data, want) == 0) : (ok == FALSE && out == NULL);
if (!res)
n_log(LOG_ERR, "got: %s", out ? out->data : "(nothing)");
free_nstr(&out);
return res;
}
static void test_build(void) {
N_STR* out = NULL;
memset(&c, 0, sizeof(c));
c.name = "__Host-nsid";
c.value = "AbC-123_x";
c.path = "/";
c.secure = 1;
c.http_only = 1;
expect_true("session cookie, as the site sends it",
builds(&c, "__Host-nsid=AbC-123_x; Path=/; Secure; HttpOnly; SameSite=Lax"));
c.max_age = 0;
c.value = "";
expect_true("deletion",
builds(&c, "__Host-nsid=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=/; Secure; HttpOnly; SameSite=Lax"));
c.max_age = 3600;
expect_true("Max-Age and Strict", builds(&c, "__Host-nsid=; Max-Age=3600; Path=/; Secure; HttpOnly; SameSite=Strict"));
memset(&c, 0, sizeof(c));
c.name = "pref";
c.value = "dark";
c.domain = "example.org";
c.path = "/a b";
expect_true("plain cookie with Domain, no flags", builds(&c, "pref=dark; Path=/a b; Domain=example.org"));
/* injection and invalid values */
c.domain = NULL;
c.path = NULL;
c.value = "a;b";
expect_true("';' in a value refused", builds(&c, NULL));
c.value = "a\r\nSet-Cookie: x=1";
expect_true("CRLF in a value refused", builds(&c, NULL));
c.value = "a b";
expect_true("space in a value refused", builds(&c, NULL));
c.value = "\"q\"";
expect_true("quotes in a value refused", builds(&c, NULL));
c.value = "ok";
c.name = "a=b";
expect_true("'=' in a name refused", builds(&c, NULL));
c.name = "";
expect_true("empty name refused", builds(&c, NULL));
c.name = NULL;
expect_true("NULL name refused", builds(&c, NULL));
c.name = "ok";
c.path = "/x;Domain=evil";
expect_true("';' in a path refused", builds(&c, NULL));
c.path = "/x\n";
expect_true("control byte in a path refused", builds(&c, NULL));
c.path = "";
expect_true("empty path refused", builds(&c, NULL));
c.path = NULL;
c.domain = "evil.org; Secure";
expect_true("bad domain refused", builds(&c, NULL));
c.domain = NULL;
c.max_age = -5;
expect_true("negative Max-Age refused", builds(&c, NULL));
expect_true("SameSite=None without Secure refused", builds(&c, NULL));
c.secure = 1;
expect_true("SameSite=None with Secure", builds(&c, "ok=ok; Secure; SameSite=None"));
expect_true("unknown SameSite refused", builds(&c, NULL));
/* cookie prefixes */
c.name = "__Host-x";
c.path = "/";
c.secure = 0;
expect_true("__Host- without Secure refused", builds(&c, NULL));
c.secure = 1;
c.path = "/admin";
expect_true("__Host- with Path other than / refused", builds(&c, NULL));
c.path = NULL;
expect_true("__Host- without Path refused", builds(&c, NULL));
c.path = "/";
c.domain = "example.org";
expect_true("__Host- with Domain refused", builds(&c, NULL));
c.domain = NULL;
c.name = "__Secure-x";
c.secure = 0;
expect_true("__Secure- without Secure refused", builds(&c, NULL));
c.secure = 1;
expect_true("__Secure- with Secure", builds(&c, "__Secure-x=ok; Path=/; Secure"));
/* appends; a refused cookie leaves the destination unchanged */
out = char_to_nstr("X: ");
c.name = "a";
c.path = NULL;
c.secure = 0;
expect_true("append", n_http_set_cookie_build(&out, &c) == TRUE && strcmp(out->data, "X: a=ok") == 0);
c.value = "b;c";
expect_true("refused append returns FALSE", n_http_set_cookie_build(&out, &c) == FALSE);
expect_true("refused append leaves the destination", strcmp(out->data, "X: a=ok") == 0);
free_nstr(&out);
expect_true("NULL arguments", n_http_set_cookie_build(NULL, &c) == FALSE && n_http_set_cookie_build(&out, NULL) == FALSE);
}
int main(int argc, char** argv) {
if (process_args(argc, argv) != 0) {
fprintf(stderr, "usage: %s [-V LOG_LEVEL]\n", argv[0]);
return 1;
}
if (failures) {
n_log(LOG_ERR, "ex_http_cookie: %d failure(s)", failures);
return 1;
}
n_log(LOG_NOTICE, "ex_http_cookie: all checks passed");
return 0;
}
void process_args(int argc, char **argv)
static int failures
Definition ex_atlas.c:51
int main(void)
Definition ex_atlas.c:117
static void expect_true(const char *label, int cond)
Definition ex_atlas.c:53
static int log_level
static int h(void *ctx, const N_ROUTE_MATCH *m)
a handler that does nothing (the test only routes)
#define n_log(__LEVEL__,...)
Logging function wrapper to get line and func.
Definition n_log.h:113
#define LOG_ERR
error conditions
Definition n_log.h:100
void set_log_level(const int log_level)
Set the global log level value ( static int LOG_LEVEL ).
Definition n_log.c:123
#define LOG_NOTICE
normal but significant condition
Definition n_log.h:104
#define LOG_INFO
informational
Definition n_log.h:106
int http_only
non-zero: HttpOnly
const char * value
cookie value, RFC 6265 cookie-octets only (may be "")
int secure
non-zero: Secure
const char * path
Path attribute, or NULL.
const char * name
cookie name, an RFC 9110 token
const char * domain
Domain attribute, or NULL (host-only cookie).
N_HTTP_SAMESITE same_site
SameSite attribute.
long long max_age
Max-Age in seconds; N_HTTP_COOKIE_SESSION for a session cookie; 0 deletes the cookie (Max-Age=0 plus ...
size_t n_http_cookie_count(const char *header, size_t len, const char *name)
count the pairs named name in a Cookie header value
N_HTTP_SAMESITE
SameSite attribute of a Set-Cookie.
int n_http_set_cookie_build(N_STR **out, const N_HTTP_SET_COOKIE *c)
append a Set-Cookie header value, refusing anything that could break out of an attribute or that a br...
#define N_HTTP_COOKIE_SESSION
Max-Age value meaning "session cookie": neither Max-Age nor Expires is sent.
const char * n_http_cookie_get(const char *header, size_t len, const char *name, size_t *value_len)
find a cookie in a Cookie header value (first occurrence wins)
@ N_HTTP_SAMESITE_NONE
SameSite=None, needs Secure.
@ N_HTTP_SAMESITE_STRICT
SameSite=Strict.
@ N_HTTP_SAMESITE_LAX
SameSite=Lax.
@ N_HTTP_SAMESITE_UNSET
no SameSite attribute (browser default)
what a Set-Cookie should carry
char * data
the string
Definition n_str.h:63
#define free_nstr(__ptr)
free a N_STR structure and set the pointer to NULL
Definition n_str.h:203
N_STR * char_to_nstr(const char *src)
Convert a char into a N_STR, short version.
Definition n_str.c:262
A box including a string and his lenght.
Definition n_str.h:61
Common headers and low-level functions & define.
Generic log system.
N_STR and string function declaration.