Skip to content

Unknown Successful Response Cache Policy Is Not Globally Enforced #2003

@LiD0209

Description

@LiD0209

Unknown Successful Response Cache Policy Is Not Globally Enforced

Result

The RFC forbids caching unrecognized successful response codes. The reviewed libcoap proxy observe-cache insertion path does not show a guard that rejects unrecognized 2.xx response codes before storing the response.

Standard Basis

RFC link: RFC 7252 Section 5.6, Caching

Unlike HTTP, the cacheability of CoAP responses does not depend on the
request method, but it depends on the Response Code. The cacheability of each
Response Code is defined along the Response Code definitions in Section 5.9.
Response Codes that indicate success and are unrecognized by an endpoint
MUST NOT be cached.

Source Evidence

Source file: src/coap_proxy.c

obs_opt = coap_check_option(rcvd, COAP_OPTION_OBSERVE, &opt_iter);

/* See if we are doing proxy caching */
if (obs_opt) {
  coap_proxy_cache_t *proxy_cache;
  coap_cache_key_t *cache_key_l;

  /* Need to cache the response */
  if (proxy_req->proxy_cache) {
    coap_delete_pdu_lkd(proxy_req->proxy_cache->rsp_pdu);
    proxy_cache = proxy_req->proxy_cache;
  } else {
    proxy_cache = coap_malloc_type(COAP_STRING, sizeof(coap_proxy_cache_t));
    /* ... */
    PROXY_CACHE_ADD(proxy_entry->rsp_cache, proxy_cache);
    proxy_cache->ref++;
  }
}

Source file: src/coap_cache.c

entry = coap_malloc_type(COAP_CACHE_ENTRY, sizeof(coap_cache_entry_t));
memset(entry, 0, sizeof(coap_cache_entry_t));
entry->session = session;
if (record_pdu == COAP_CACHE_RECORD_PDU) {
  entry->pdu = coap_const_pdu_reference_lkd(pdu);
}
entry->cache_key = coap_cache_derive_key(session, pdu, session_based);
HASH_ADD(hh, session->context->cache, cache_key[0], sizeof(coap_cache_key_t), entry);
return entry;

Comparison

The standard requires a negative cacheability decision when a success response code is not recognized. The proxy cache insertion code is driven by the Observe option and proxy-cache state; the reviewed block does not inspect rcvd->code before adding the response to rsp_cache. The generic cache-entry API derives a key and inserts an entry but likewise does not evaluate whether the stored response has an unrecognized success code.

This is a partial mismatch: libcoap provides cache mechanisms, but the audited cache paths do not show a global no-cache guard for unknown successful response codes.

Runtime Reproduction

The static finding was rechecked with a dedicated origin -> libcoap proxy -> two clients runtime harness:

  • repro program: test-libcoap/251-300/repro_id287_unknown_success_cache_runtime.c
  • repro binary: test-libcoap/251-300/repro_id287_unknown_success_cache_runtime_v2.exe
  • repro log: test-libcoap/251-300/repro_id287_unknown_success_cache_runtime.log

Important correction for this runtime check: on this source snapshot reviewed on 2026-05-06, libcoap already defines 2.31 as COAP_RESPONSE_CODE_CONTINUE in include/coap3/coap_pdu.h, so 2.31 is not an unknown success code for this build. To test the RFC condition accurately, the runtime harness used 2.06, which is a success-class code not defined in the reviewed libcoap response-code table.

The harness sets up:

  • an origin server resource /unknown-success returning 2.06 + Observe + Max-Age=90 + payload unknown-success-v1
  • the same origin server resource /known-success returning 2.05 Content + Observe + Max-Age=90 + payload known-success-v1
  • a libcoap forward proxy using coap_resource_proxy_uri_init2() and coap_proxy_forward_request()
  • two clients that both send GET with Observe=0 through the same proxy

The decisive signals are whether the second client receives the cached response and whether the origin hit counter remains 1.

Runtime Log Excerpt

CASE_BEGIN label=unknown proxy_uri=coap://127.0.0.1:5691/unknown-success expected_code=2.06 payload=unknown-success-v1
ORIGIN_HIT label=unknown hit=1 observe=1 code=2.06 payload=unknown-success-v1
CLIENT_RESPONSE name=client1 code=2.06 observe=1 max_age=90 payload=unknown-success-v1
CLIENT_RESPONSE name=client2 code=2.06 observe=1 max_age=90 payload=unknown-success-v1
CASE_RESULT label=unknown client1_code=2.06 client2_code=2.06 client2_payload=unknown-success-v1 origin_hits_after_client2=1 cached=1
unknown_2xx_cached=1 origin_hits_after_client2=1 client2_code=2.06 client2_payload=unknown-success-v1

CASE_BEGIN label=known proxy_uri=coap://127.0.0.1:5693/known-success expected_code=2.05 payload=known-success-v1
ORIGIN_HIT label=known hit=1 observe=1 code=2.05 payload=known-success-v1
CLIENT_RESPONSE name=client1 code=2.05 observe=1 max_age=90 payload=known-success-v1
CLIENT_RESPONSE name=client2 code=2.05 observe=1 max_age=90 payload=known-success-v1
CASE_RESULT label=known client1_code=2.05 client2_code=2.05 client2_payload=known-success-v1 origin_hits_after_client2=1 cached=1
known_205_cached=1 origin_hits_after_client2=1 client2_code=2.05 client2_payload=known-success-v1

Runtime Interpretation

The control case confirms that the proxy observe-cache path is functioning normally for a recognized cacheable success response: known_205_cached=1 and the origin hit counter stays at 1 after the second client request.

The unknown-success case shows the same replay behavior for an unrecognized success-class code: the second client receives 2.06 with the original payload unknown-success-v1, and origin_hits_after_client2 remains 1. That means the response was served from the proxy cache rather than refetched from the origin.

This runtime outcome matches the static review: the libcoap proxy observe-cache machinery caches and replays an unrecognized 2.xx success response instead of enforcing the RFC 7252 Section 5.6 rule that such responses MUST NOT be cached.

Test Result

source_assertions_251_300.py
PASS payload_marker_followed_by_zero_length_rejected
PASS nonzero_payload_encoder_adds_marker
PASS payload_pointer_derived_after_marker
PASS proxy_uri_parser_uses_proxy_mode
PASS uri_split_options_supported
PASS proxy_unavailable_returns_505
PASS proxy_endpoint_authority_can_be_local
PASS delete_unknown_resource_returns_202
PASS bad_option_error_response_path
PASS reserved_response_class_3_accepted
PASS etag_match_large_response_sets_203_without_payload
PASS proxy_forward_response_uses_large_response_helper
PASS proxy_observe_cache_insertion_present
PASS proxy_observe_cache_has_no_response_code_guard
PASS proxy_cleanup_uses_502_not_504
PASS gateway_timeout_code_defined
PASS cn_authority_validation_delegated_to_application
ALL ASSERTIONS PASSED

repro_251_300_protocol_checks.exe
PASS payload marker followed by zero-length payload is rejected
PASS payload marker followed by payload is accepted
PASS payload length is derived from remaining datagram bytes
PASS non-zero payload is encoded with 0xff marker
PASS zero-length payload does not encode payload marker
PASS Proxy-Uri parser accepts HTTP URI for proxy use
PASS origin URI parser rejects proxy-only HTTP URI
PASS origin URI parser splits coap URI authority/path/query
ALL PROTOCOL CHECKS PASSED

repro_id287_unknown_success_cache_runtime_v2.exe
CASE_BEGIN label=unknown proxy_uri=coap://127.0.0.1:5691/unknown-success expected_code=2.06 payload=unknown-success-v1
ORIGIN_HIT label=unknown hit=1 observe=1 code=2.06 payload=unknown-success-v1
CLIENT_RESPONSE name=client1 code=2.06 observe=1 max_age=90 payload=unknown-success-v1
CLIENT_RESPONSE name=client2 code=2.06 observe=1 max_age=90 payload=unknown-success-v1
unknown_2xx_cached=1 origin_hits_after_client2=1 client2_code=2.06 client2_payload=unknown-success-v1
CASE_BEGIN label=known proxy_uri=coap://127.0.0.1:5693/known-success expected_code=2.05 payload=known-success-v1
ORIGIN_HIT label=known hit=1 observe=1 code=2.05 payload=known-success-v1
CLIENT_RESPONSE name=client1 code=2.05 observe=1 max_age=90 payload=known-success-v1
CLIENT_RESPONSE name=client2 code=2.05 observe=1 max_age=90 payload=known-success-v1
known_205_cached=1 origin_hits_after_client2=1 client2_code=2.05 client2_payload=known-success-v1

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions