urldecode: add validation for URL encoding
What changed, and why it matters
This commit hardens a URL-decoding helper in Blockstream Jade's firmware. It adds validation so malformed percent-encoding, embedded null bytes, control characters, and non-ASCII bytes are rejected instead of being silently decoded. A new 'dry run' validation function is also exposed so callers can check an encoded string before decoding it. The change is defensive: it reduces the chance that an attacker could smuggle dangerous characters into URLs or trick downstream code with truncated or malformed strings.
Review all callers of urldecode() and is_valid_urlencoding() to ensure they handle the new false-return cases safely (e.g. do not fall back to using unvalidated input). Confirm that the printable-ASCII restriction (0x20-0x7e) does not break legitimate URL parameters such as internationalized domain names or non-English labels. Consider adding unit tests for malformed % sequences, embedded NULs, and buffer-overflow scenarios.
Security signals we found
Input validation added for percent-encoded sequences
Rejection of embedded NUL bytes and control characters in decoded output
Rejection of non-ASCII decoded bytes (>=0x80 and DEL 0x7f)
Removal of silent output-buffer truncation
New dry-run validation API is_valid_urlencoding()
Callback hook for caller-specific character filtering
Evidence from the diff
The patch refactors urldecode() into a shared try_urldecode() implementation and adds is_valid_urlencoding(). Key changes: (1) rejects invalid %XX sequences (e.g. ‘%’ not followed by two hex digits, or ‘%’ near end of buffer); (2) rejects decoded bytes outside printable ASCII range 0x20-0x7e, blocking embedded NULs, control chars, high bytes, and DEL; (3) requires length-specified inputs to be NUL-padded after the first NUL; (4) removes silent truncation on output-buffer overflow, returning false instead; (5) adds an optional caller-supplied check_fn callback for per-character filtering. The public wrapper urldecode() keeps the same signature but now delegates to the stricter core.
Changed components
main/utils/urldecode.cmain/utils/urldecode.hURL decoding utility used by Jade firmwareInspect captured patch +100 / −27
### main/utils/urldecode.c
@@ -23,51 +23,99 @@ static char map_char(char c)
return c;
}
-// Simple urldecode function - any triple that looks like "%,hex,hex" is replaced by the character
-// specified, a '+' is replaced by a space, and anything else is copied verbatim.
-// The output string is always nul-terminated (although the input need not be).
-bool urldecode(const char* src, const size_t src_len, char* dest, const size_t dest_len)
+// Core implementation of URL decoding.
+// Destination buffer, 'dest' is allowed to be NULL for "dry run" processing (source validation).
+// Optional 'check_fn', if not NULL, is called for each decoded character for additional validation.
+static bool try_urldecode(
+ const char* src, const size_t src_len, char* dest, const size_t dest_len, int (*check_fn)(int))
{
- JADE_ASSERT(src);
- JADE_ASSERT(src_len);
- JADE_ASSERT(dest);
- JADE_ASSERT(dest_len);
+ // We don't use assertions to give more flexibility for using this function to validate encoded
+ // string with uncertain arguments (like validating optional parameters that are absent atm).
+ if (!src || !src_len || !dest_len) {
+ return false;
+ }
const char* src_end = src + src_len;
- const char* dest_end = dest + dest_len;
+ char* dest_start = dest;
+ size_t decoded_len = 0;
- // Handle both terminated and length-specified string data
- while (src < src_end && *src) {
- if (dest == dest_end - 1) {
- // Destination insufficient - need last location for nul-terminator.
- // Truncate (terminate) here and return false.
- *dest = '\0';
+ // Handle both nul-terminated and length-specified string data.
+ // Nul-terminated string is accepted only if it has no nonzero characters after the terminator.
+ while (src < src_end) {
+ if (*src == '\0') {
+ // Verify that a nul-terminated string is nul-padded
+ while (src < src_end) {
+ if (*src++ != '\0') {
+ return false;
+ }
+ }
+ break; // terminate processing of input string
+ }
+
+ if (decoded_len + 1 == dest_len) {
+ // Destination insufficient - need last location for nul-terminator
return false;
}
- if ((*src == '%') && (src_end - src > 2) && isxdigit((unsigned char)src[1])
- && isxdigit((unsigned char)src[2])) {
- // Encoded hex character
- *dest++ = (16 * map_char(src[1])) + map_char(src[2]);
+ int decoded;
+ if (*src == '%') {
+ // Ensure we have at least 2 more characters in the input string and both are hex digits
+ if ((src_end - src <= 2) || !isxdigit((unsigned char)src[1]) || !isxdigit((unsigned char)src[2])) {
+ return false;
+ }
+
+ // Fetch 3 input characters and hex decode them into a single output character
+ decoded = (16 * map_char(src[1])) + map_char(src[2]);
src += 3;
} else if (*src == '+') {
// Encoded <space>
- *dest++ = ' ';
+ decoded = ' ';
++src;
} else {
- // Copy across
- *dest++ = *src++;
+ // Assign "as is" (avoiding signed promotion)
+ decoded = (unsigned char)*src++;
+ }
+
+ // Ensure the character falls in the allowed range: <space>...~
+ if (decoded < 0x20 || decoded > 0x7e) {
+ // Reject embedded nul, control characters, non-ASCII bytes and DEL
+ return false;
}
+
+ // Use callback function for filtering decoded character if given
+ if (check_fn && !check_fn(decoded)) {
+ return false; // rejected by caller-provided check function: fail
+ }
+
+ // Copy decoded character into output buffer if we aren't in the "dry run" mode
+ if (dest) {
+ *dest++ = decoded;
+ }
+ ++decoded_len;
}
- JADE_ASSERT(dest < dest_end);
- *dest = '\0';
+ // Sanity check: ensure all input characters are processed
+ JADE_ASSERT(src == src_end);
+ // Sanity check: ensure we didn't overrun the output buffer
+ JADE_ASSERT(decoded_len < dest_len);
+ // Nul-terminate the output string if we aren't in the "dry run" mode
+ if (dest) {
+ // Sanity check: destination pointer is consistent with decoded counter
+ JADE_ASSERT(dest == dest_start + decoded_len);
+ *dest = '\0';
+ }
return true;
}
-// Simple urlencode function - special chars are replaced by %XX, space is replaced by '+',
-// and anything else is copied verbatim.
-// The output string is always nul-terminated (although the input need not be).
+bool urldecode(const char* src, const size_t src_len, char* dest, const size_t dest_len)
+{
+ JADE_ASSERT(src);
+ JADE_ASSERT(src_len);
+ JADE_ASSERT(dest);
+ JADE_ASSERT(dest_len);
+ return try_urldecode(src, src_len, dest, dest_len, NULL);
+}
+
bool urlencode(const char* src, const size_t src_len, char* dest, const size_t dest_len)
{
JADE_ASSERT(src);
@@ -112,4 +160,11 @@ bool urlencode(const char* src, const size_t src_len, char* dest, const size_t d
*dest = '\0';
return true;
}
+
+bool is_valid_urlencoding(const char* src, const size_t src_len, const size_t max_len, int (*check_fn)(int))
+{
+ JADE_ASSERT(max_len);
+ return try_urldecode(src, src_len, NULL, max_len, check_fn);
+}
+
#endif // AMALGAMATED_BUILD
### main/utils/urldecode.h
@@ -6,7 +6,25 @@
#include "../jade_assert.h"
+// Simple urldecode function - any triple that looks like "%XX" is replaced by the character
+// specified, a '+' is replaced by a space, and any printable characters are copied verbatim.
+// Both nul-terminated and length-specified input strings are supported but in latter case they must
+// be nul-padded.
+// The function returns true in case of success and false if there is an error in buffer sizes or
+// invalid encoding, including: embedded nul, or hex encoded characters outside 0x20-0x7f range.
+// In case of success the output string is always nul-terminated (although the input need not be).
WARN_UNUSED_RESULT bool urldecode(const char* src, size_t src_len, char* dest, size_t dest_len);
+
+// Simple urlencode function - special chars are replaced by %XX, space is replaced by '+',
+// and anything else is copied verbatim.
+// The output string is always nul-terminated (although the input need not be).
WARN_UNUSED_RESULT bool urlencode(const char* src, size_t src_len, char* dest, size_t dest_len);
+// Simple validation function for URL encoding, designed to operate in the same way as urldecode()
+// with an optional capability to filter decoded characters with a user-provided callback
+// function (can be NULL). The argument 'max_len' is used to limit the number of characters checked.
+// Both nul-terminated and length-specified input strings are supported but in latter case they must
+// be nul-padded.
+WARN_UNUSED_RESULT bool is_valid_urlencoding(const char* src, size_t src_len, size_t max_len, int (*check_fn)(int));
+
#endif /* UTILS_URLDECODE_H_ */Why this scored 45/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.