Security model
Importance of security in a Protocol Buffers library
In the context of protocol buffers, security comes into play when decoding untrusted data. Naturally, if the attacker can modify the contents of a protocol buffers message, he can feed the application any values possible. Therefore the application itself must be prepared to receive untrusted values.
Where nanopb plays a part is preventing the attacker from running arbitrary code on the target system. Mostly this means that there must not be any possibility to cause buffer overruns, memory corruption or invalid pointers by the means of crafting a malicious message.
Division of trusted and untrusted data
The following data is regarded as trusted. It must be under the control of the application writer. Malicious data in these structures could cause security issues, such as execution of arbitrary code:
- Callback, pointer and extension fields in message structures given
to
pb_encode()andpb_decode(). These fields are memory pointers, and are generated depending on the message definition in the.protofile. - The automatically generated field definitions, i.e.
pb_msgdesc_t. - The
pb_decode_ctx_tandpb_encode_ctx_tstructures (this does not mean the contents of the stream itself, just the pointers and other fields in the context structure).
The following data is regarded as untrusted. Invalid/malicious data in these will cause “garbage in, garbage out” behaviour. It will not cause buffer overflows, information disclosure or other security problems:
- All data read from the input stream represented by
pb_decode_ctx_t. - All fields in message structures, except:
- callbacks (
pb_callback_tstructures) - pointer fields and
_countfields for pointers - extensions (
pb_extension_tstructures)
- callbacks (
Invariants
The following invariants are maintained during operation, even if the untrusted data has been maliciously crafted:
- Nanopb will never read more than
bytes_leftbytes frompb_decode_ctx_t. - Nanopb will never write more than
max_sizebytes topb_encode_ctx_t. - Nanopb will never access memory out of bounds of the message structure.
- After
pb_decode()returns successfully, the message structure will be internally consistent:- The
countfields of arrays will not exceed the array size. - The
sizefield of bytes will not exceed the allocated size. - All string fields will have null terminator.
boolfields will have valid true/false values- pointer fields will be either
NULLor point to valid data
- The
- After
pb_encode()returns successfully, the resulting message is a valid protocol buffers message. (Except if user-defined callbacks write incorrect data.) - All memory allocated by
pb_decode()will be released by a subsequent call topb_release()on the same message.
Further considerations
Even if the nanopb library is free of any security issues, there are still several possible attack vectors that the application author must consider. The following list is not comprehensive:
- Stack usage may depend on the contents of the message. The message definition places an upper bound on how much stack will be used. Tests should be run with all fields present, to record the maximum possible stack usage.
- Callbacks can do anything. The code for the callbacks must be carefully checked if they are used with untrusted data.
- If using stream input, a maximum size should be set in
pb_decode_ctx_tto stop a denial of service attack from using an infinite message. - If using network sockets as streams, a timeout should be set to stop denial of service attacks.
- If using
malloc()support, some method of limiting memory use should be employed. This can be done by defining custompb_realloc()function. Nanopb will properly detect and handle failed memory allocations.
Security hardening
Following steps will add deference-in-depth protections that make successful exploitation harder, even if there were an undiscovered vulnerability in nanopb or in the user application:
-
Enable compiler memory protection features, such as
-fstack-protectoror-fsanitize=address. -
Disable unneeded features using compilation option
PB_MINIMALor the individual feature disables. This reduces the attack surface. In particular the following options should be considered for security-critical applications:PB_NO_STRUCT_FIELD_CALLBACK = 1: Storing callbacks inline with structure data creates a pathway to exploitation on a buffer overflow.PB_NO_EXTENSIONS = 1: To a lesser extent, the pointer to extension definition can be used as exploitation pathway after another bug has caused memory corruption.PB_NO_RECURSION = 1: Disabling recursion makes the stack usage bounded and largely independent of message contents.PB_NO_FUNCTION_POINTERS = 1: Disabling function-pointer usage reduces exploitation methods if a buffer overflow were to occur.PB_NO_MALLOC = 1: Disable dynamic allocation if it is not needed. Alternatively, use a custom arena allocator to reduce risks of heap corruption or malicious heap fragmentation.PB_NO_OPT_ASSERT = 0: Keeping optional asserts enables can detect internal errors before they lead to a security problem.
-
Set
max_sizelimits to all output streams andbytes_leftlimits to input streams. The generator createsMyMessage_sizedefines that indicate the maximum expected size for a particular message type. -
Perform fuzzing against your application. Start with valid messages and use a profiling-guided fuzzer such as afl-fuzz to trigger otherwise dormant code paths.