commit 55b7ddabb83475cc954a0ffa9f141d2b6d0abaf8
parent b00b4468dd51dc1c9b80b6dc687e3dbde744cdf0
Author: Christian Grothoff <christian@grothoff.org>
Date: Thu, 21 May 2026 22:11:09 +0200
add slug-validation in JSON spec parser"
Diffstat:
5 files changed, 121 insertions(+), 1 deletion(-)
diff --git a/meson.build b/meson.build
@@ -305,7 +305,7 @@ if not get_option('only-doc')
libltversions = [
['libtalerutil', '12:0:2'],
- ['libtalerjson', '6:0:2'],
+ ['libtalerjson', '7:0:3'],
['libtalerextensions', '0:0:0'],
['libtalercurl', '0:1:0'],
['libtalerpq', '0:1:0'],
diff --git a/src/include/taler/taler_json_lib.h b/src/include/taler/taler_json_lib.h
@@ -601,6 +601,18 @@ TALER_JSON_spec_web_url (const char *field,
/**
+ * Generate line in parser specification for slugs (URL-safe identifiers).
+ *
+ * @param field name of the field
+ * @param[out] slug string to initialize
+ * @return corresponding field spec
+ */
+struct GNUNET_JSON_Specification
+TALER_JSON_spec_slug (const char *field,
+ const char **slug);
+
+
+/**
* Generate line in parser specification for full
* "payto://" URIs.
*
diff --git a/src/include/taler/taler_util.h b/src/include/taler/taler_util.h
@@ -439,6 +439,30 @@ TALER_is_web_url (const char *url);
/**
+ * Test if the URL is a valid slug (URL-safe string).
+ *
+ * Allowed characters:
+ * - ASCII letters: a-z A-Z
+ * - Digits: 0-9
+ * - Hyphen: -
+ * - Underscore: _
+ * - Period: .
+ * - Tilde: ~
+ *
+ * Additional restrictions:
+ * - must not be empty
+ * - must not be "." or ".."
+ * - must not contain '/'
+ * - must not contain percent-encoding '%'
+ *
+ * @param slug a string to test if it could be a valid slug
+ * @return true if @a slug is well-formed
+ */
+bool
+TALER_is_slug (const char *slug);
+
+
+/**
* Check if @a lang matches the @a language_pattern, and if so with
* which preference.
* See also: https://tools.ietf.org/html/rfc7231#section-5.3.1
diff --git a/src/json/json_helper.c b/src/json/json_helper.c
@@ -1478,6 +1478,53 @@ TALER_JSON_spec_web_url (const char *field,
/**
+ * Parse given JSON object to slug.
+ *
+ * @param cls closure, NULL
+ * @param root the json object representing data
+ * @param[out] spec where to write the data
+ * @return #GNUNET_OK upon successful parsing; #GNUNET_SYSERR upon error
+ */
+static enum GNUNET_GenericReturnValue
+parse_slug (void *cls,
+ json_t *root,
+ struct GNUNET_JSON_Specification *spec)
+{
+ const char *str;
+
+ (void) cls;
+ str = json_string_value (root);
+ if (NULL == str)
+ {
+ GNUNET_break_op (0);
+ return GNUNET_SYSERR;
+ }
+ if (! TALER_is_slug (str))
+ {
+ GNUNET_break_op (0);
+ return GNUNET_SYSERR;
+ }
+ *(const char **) spec->ptr = str;
+ return GNUNET_OK;
+}
+
+
+struct GNUNET_JSON_Specification
+TALER_JSON_spec_slug (const char *field,
+ const char **slug)
+{
+ struct GNUNET_JSON_Specification ret = {
+ .parser = &parse_slug,
+ .field = field,
+ .ptr = slug
+ };
+
+ *slug = NULL;
+ return ret;
+}
+
+
+/**
* Parse given JSON object to payto:// URI.
*
* @param cls closure, NULL
diff --git a/src/util/util.c b/src/util/util.c
@@ -460,6 +460,43 @@ TALER_words_destroy (char **args)
}
+bool
+TALER_is_slug (const char *slug)
+{
+ const unsigned char *p;
+
+ if ('\0' == slug[0])
+ return false;
+
+ /* Reject special path components */
+ if (0 == strcmp (slug, ".") ||
+ 0 == strcmp (slug, ".."))
+ return false;
+
+ for (p = (const unsigned char *) slug; '\0' != *p; p++)
+ {
+ unsigned char c = *p;
+
+ if (isalnum (c))
+ continue;
+
+ switch (c)
+ {
+ case '-':
+ case '_':
+ case '.':
+ case ':':
+ /* Note: could also allow '~' in principle, is on safe list! */
+ continue;
+ default:
+ return false;
+ }
+ }
+
+ return true;
+}
+
+
#ifdef __APPLE__
char *
strchrnul (const char *s,