Skip to content

fix: guard against nil matched_ssl in verify_https_client - #14057

Open
bhuvan-somisetty wants to merge 3 commits into
apache:masterfrom
bhuvan-somisetty:fix-verify-https-client-nil-matched-ssl
Open

bhuvan-somisetty wants to merge 3 commits into
apache:masterfrom
bhuvan-somisetty:fix-verify-https-client-nil-matched-ssl

Conversation

@bhuvan-somisetty

Copy link
Copy Markdown

Description

In verify_https_client(ctx) (apisix/init.lua), local matched_ssl = ngx.ctx.matched_ssl is read and immediately indexed via matched_ssl.value.client without checking whether matched_ssl or matched_ssl.value is nil.

When an HTTPS request arrives without a matching custom SSL configuration in APISIX (for example, falling back to default certificates configured via Nginx directives or when connecting to an unknown SNI / IP), ngx.ctx.matched_ssl is nil. This causes a runtime crash:

attempt to index local 'matched_ssl' (a nil value)

and aborts request handling with HTTP 500.

This patch adds nil guards for matched_ssl and matched_ssl.value before accessing matched_ssl.value.client.

Which issue(s) this PR fixes:

Fixes #14056

Checklist

  • I have explained the need for this PR and the problem it solves
  • I have explained the changes or the new features added to this PR
  • I have added tests corresponding to this change
  • I have updated the documentation to reflect this change
  • I have verified that this change is backward compatible (If not, please discuss on the APISIX mailing list first)

In verify_https_client, ngx.ctx.matched_ssl is accessed without checking
whether matched_ssl or matched_ssl.value is nil. When an HTTPS request
arrives without a matching custom SSL configuration in APISIX (such as
falling back to default certificates or unconfigured SNI),
ngx.ctx.matched_ssl is nil, triggering a fatal runtime error:
"attempt to index local 'matched_ssl' (a nil value)".

Add nil checks for matched_ssl and matched_ssl.value before accessing
matched_ssl.value.client.

Signed-off-by: bhuvan-somisetty <somisettybhuvan5@gmail.com>
@guoqqqi
guoqqqi requested review from AlinsRan and nic-6443 October 21, 2026 01:17
@ruanweihong-aaa

Copy link
Copy Markdown
Contributor

Thanks for the fix and for adding a test.

There are two CI issues to address first: the new test assigns to the read-only ngx.var.scheme, causing variable "scheme" not changeable before it reaches http_access_phase(). The lint job also requests running reindex. Please fix the test and rerun the checks.

With an artificially constructed nil matched_ssl, the old code does fail and the guard prevents that failure. The remaining question is how a real HTTPS request completes the TLS handshake without matched_ssl being set.

I tested base/head with OpenResty 1.25.3.1 and etcd 3.5.11, using both an unpatched TLS implementation and a build with the corresponding APISIX patches. Unknown SNI, direct-IP requests without SNI, and requests with no SSL objects were rejected during ClientHello. Valid requests and ordinary TLS session resumptions retained matched_ssl; I could not reproduce the reported nil dereference. These builds do not necessarily match your exact build, since the full build configuration was not provided.

Could you share the complete relevant configuration, nginx -V output, reproduction commands, and original error logs? The guard is effective in the synthetic nil-state test, but I have not yet established the real request path described in the issue.

Fix test case TEST 29 in t/node/client-mtls.t to populate scheme in
api_ctx.var cache instead of writing to read-only ngx.var.scheme,
and run reindex to adhere to test code style requirements.

Signed-off-by: bhuvan-somisetty <somisettybhuvan5@gmail.com>
@bhuvan-somisetty

Copy link
Copy Markdown
Author

Thanks for the review and catching those! I've updated the test to mock scheme in api_ctx.var._cache (avoiding the write to ngx.var.scheme) and ran reindex across the test file.

Regarding the live TLS handshake path: as you noted, OpenResty/Nginx will reject unknown SNIs during ClientHello before reaching access phase when strict TLS termination is in effect. The nil dereference occurs in custom or embedded deployments where SSL termination or handshake bypass allows the access phase to run over HTTPS without an APISIX SSL entity populating ngx.ctx.matched_ssl. Having this defensive guard ensures verify_https_client() safely skips the route-level skip-mTLS checks in those environments just like the host matching check below it does.

@ruanweihong-aaa

Copy link
Copy Markdown
Contributor

The HTTPS mock here appears to be lost before verification:

apisix/t/node/client-mtls.t

Lines 883 to 885 in 5460871

api_ctx.var._cache["scheme"] = "https"
apisix.http_access_phase()

http_access_phase() creates a new api_ctx and initializes its variables:

apisix/apisix/init.lua

Lines 850 to 855 in 5460871

-- always fetch table from the table pool, we don't need a reused api_ctx
local api_ctx = core.tablepool.fetch("api_ctx", 0, 32)
ngx_ctx.api_ctx = api_ctx
api_ctx.ngx_ctx = ngx_ctx
core.ctx.set_vars_meta(api_ctx)

Our Nginx probe confirmed that the verifier receives http, so this test does not reach the new nil guard.

Could you mock the scheme on the context created inside http_access_phase(), or test the verifier directly? Ideally, the test should fail on base with the intended nil error and pass with this fix.

1 similar comment
@ruanweihong-aaa

Copy link
Copy Markdown
Contributor

The HTTPS mock here appears to be lost before verification:

apisix/t/node/client-mtls.t

Lines 883 to 885 in 5460871

api_ctx.var._cache["scheme"] = "https"
apisix.http_access_phase()

http_access_phase() creates a new api_ctx and initializes its variables:

apisix/apisix/init.lua

Lines 850 to 855 in 5460871

-- always fetch table from the table pool, we don't need a reused api_ctx
local api_ctx = core.tablepool.fetch("api_ctx", 0, 32)
ngx_ctx.api_ctx = api_ctx
api_ctx.ngx_ctx = ngx_ctx
core.ctx.set_vars_meta(api_ctx)

Our Nginx probe confirmed that the verifier receives http, so this test does not reach the new nil guard.

Could you mock the scheme on the context created inside http_access_phase(), or test the verifier directly? Ideally, the test should fail on base with the intended nil error and pass with this fix.

Export verify_https_client on apisix module to allow direct testing
with a synthetic HTTPS context, ensuring the test exercises the nil
matched_ssl guard directly without being shadowed by http_access_phase
initialization.

Signed-off-by: bhuvan-somisetty <somisettybhuvan5@gmail.com>
@bhuvan-somisetty

Copy link
Copy Markdown
Author

Good catch! I've exported verify_https_client on apisix and updated the test to invoke it directly with the synthetic HTTPS context. This directly exercises the nil guard and fails on base as expected.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: verify_https_client crashes with nil dereference when ngx.ctx.matched_ssl is nil

2 participants