jq: Fix CVE-2026-43895

This patch applies the upstream fix as referenced in [2], using the commit shown in [1].

[1] https://github.com/jqlang/jq/commit/9d223f153c3632a207fa071caaa6292da33ae361
[2] https://github.com/jqlang/jq/security/advisories/GHSA-7q7g-mrq3-phxr

Signed-off-by: Darsh Kelaiya <dkelaiya@cisco.com>
Signed-off-by: Anuj Mittal <anuj.mittal@oss.qualcomm.com>
This commit is contained in:
Darsh Kelaiya
2026-07-21 02:39:35 -07:00
committed by Anuj Mittal
parent 30eaef2541
commit da15df26e6
2 changed files with 207 additions and 0 deletions
@@ -0,0 +1,206 @@
From 5192a59c2c6ca6ab0667cbee608db29c0710f346 Mon Sep 17 00:00:00 2001
From: itchyny <itchyny@cybozu.co.jp>
Date: Sat, 9 May 2026 17:08:43 +0900
Subject: [PATCH 1/2] Reject embedded NUL bytes in module import paths
jq accepts embedded NUL bytes at the language level but resolves
module import paths through NUL-terminated C strings, so the path
validated by policy or audit code could differ from the on-disk
path jq actually opens. Pass jv through gen_import so the AST
preserves the original bytes, and reject embedded NULs in
validate_relpath.
Fixes CVE-2026-43895.
CVE: CVE-2026-43895
Upstream-Status: Backport [https://github.com/jqlang/jq/commit/9d223f153c3632a207fa071caaa6292da33ae361]
Backport Changes:
- Kept jq 1.7.1 HOME lookup in linker.c.
The newer get_home() flow is absent from the target.
Only the jv-based gen_import() call was required.
- Limited parser.c to three import/include action changes.
The newer grammar caused unrelated generated-parser churn.
Keeping target numbering avoids unrelated generated changes.
(cherry picked from commit 9d223f153c3632a207fa071caaa6292da33ae361)
Signed-off-by: Darsh Kelaiya <dkelaiya@cisco.com>
---
src/compile.c | 12 ++++++++----
src/compile.h | 2 +-
src/linker.c | 6 +++++-
src/parser.c | 16 +++-------------
src/parser.y | 16 +++-------------
tests/shtest | 17 +++++++++++++++++
6 files changed, 37 insertions(+), 32 deletions(-)
diff --git a/src/compile.c b/src/compile.c
index e5e65f2..27b2cfd 100644
--- a/src/compile.c
+++ b/src/compile.c
@@ -526,13 +526,17 @@ jv block_module_meta(block b) {
return jv_null();
}
-block gen_import(const char* name, const char* as, int is_data) {
+block gen_import(jv name, jv as, int is_data) {
+ assert(jv_get_kind(name) == JV_KIND_STRING);
+ assert(!jv_is_valid(as) || jv_get_kind(as) == JV_KIND_STRING);
inst* i = inst_new(DEPS);
jv meta = jv_object();
- if (as != NULL)
- meta = jv_object_set(meta, jv_string("as"), jv_string(as));
+ if (jv_is_valid(as))
+ meta = jv_object_set(meta, jv_string("as"), as);
+ else
+ jv_free(as);
meta = jv_object_set(meta, jv_string("is_data"), is_data ? jv_true() : jv_false());
- meta = jv_object_set(meta, jv_string("relpath"), jv_string(name));
+ meta = jv_object_set(meta, jv_string("relpath"), name);
i->imm.constant = meta;
return inst_block(i);
}
diff --git a/src/compile.h b/src/compile.h
index c1512e6..bac65ef 100644
--- a/src/compile.h
+++ b/src/compile.h
@@ -33,7 +33,7 @@ block gen_op_pushk_under(jv constant);
block gen_module(block metadata);
jv block_module_meta(block b);
-block gen_import(const char* name, const char *as, int is_data);
+block gen_import(jv name, jv as, int is_data);
block gen_import_meta(block import, block metadata);
block gen_function(const char* name, block formals, block body);
block gen_param_regular(const char* name);
diff --git a/src/linker.c b/src/linker.c
index e7d1024..4b15008 100644
--- a/src/linker.c
+++ b/src/linker.c
@@ -93,6 +93,10 @@ static jv build_lib_search_chain(jq_state *jq, jv search_path, jv jq_origin, jv
// in between).
static jv validate_relpath(jv name) {
const char *s = jv_string_value(name);
+ if (strlen(s) != (size_t)jv_string_length_bytes(jv_copy(name))) {
+ jv_free(name);
+ return jv_invalid_with_msg(jv_string("Module path contains a NUL byte"));
+ }
if (strchr(s, '\\')) {
jv res = jv_invalid_with_msg(jv_string_fmt("Modules must be named by relative paths using '/', not '\\' (%s)", s));
jv_free(name);
@@ -423,7 +427,7 @@ int load_program(jq_state *jq, struct locfile* src, block *out_block) {
char* home = getenv("HOME");
if (home) { // silently ignore no $HOME
/* Import ~/.jq as a library named "" found in $HOME */
- block import = gen_import_meta(gen_import("", NULL, 0),
+ block import = gen_import_meta(gen_import(jv_string(""), jv_invalid(), 0),
gen_const(JV_OBJECT(
jv_string("optional"), jv_true(),
jv_string("search"), jv_string(home))));
diff --git a/src/parser.c b/src/parser.c
index 0599db7..c50f2fb 100644
--- a/src/parser.c
+++ b/src/parser.c
@@ -3081,13 +3081,8 @@ yyreduce:
case 50: /* ImportWhat: "import" ImportFrom "as" BINDING */
#line 506 "src/parser.y"
{
- jv v = block_const((yyvsp[-2].blk));
- // XXX Make gen_import take only blocks and the int is_data so we
- // don't have to free so much stuff here
- (yyval.blk) = gen_import(jv_string_value(v), jv_string_value((yyvsp[0].literal)), 1);
+ (yyval.blk) = gen_import(block_const((yyvsp[-2].blk)), (yyvsp[0].literal), 1);
block_free((yyvsp[-2].blk));
- jv_free((yyvsp[0].literal));
- jv_free(v);
}
#line 3093 "src/parser.c"
break;
@@ -3095,11 +3090,8 @@ yyreduce:
case 51: /* ImportWhat: "import" ImportFrom "as" IDENT */
#line 515 "src/parser.y"
{
- jv v = block_const((yyvsp[-2].blk));
- (yyval.blk) = gen_import(jv_string_value(v), jv_string_value((yyvsp[0].literal)), 0);
+ (yyval.blk) = gen_import(block_const((yyvsp[-2].blk)), (yyvsp[0].literal), 0);
block_free((yyvsp[-2].blk));
- jv_free((yyvsp[0].literal));
- jv_free(v);
}
#line 3105 "src/parser.c"
break;
@@ -3107,10 +3099,8 @@ yyreduce:
case 52: /* ImportWhat: "include" ImportFrom */
#line 522 "src/parser.y"
{
- jv v = block_const((yyvsp[0].blk));
- (yyval.blk) = gen_import(jv_string_value(v), NULL, 0);
+ (yyval.blk) = gen_import(block_const((yyvsp[0].blk)), jv_invalid(), 0);
block_free((yyvsp[0].blk));
- jv_free(v);
}
#line 3116 "src/parser.c"
break;
diff --git a/src/parser.y b/src/parser.y
index 3d24689..2901cab 100644
--- a/src/parser.y
+++ b/src/parser.y
@@ -504,26 +504,16 @@ ImportWhat Exp ';' {
ImportWhat:
"import" ImportFrom "as" BINDING {
- jv v = block_const($2);
- // XXX Make gen_import take only blocks and the int is_data so we
- // don't have to free so much stuff here
- $$ = gen_import(jv_string_value(v), jv_string_value($4), 1);
+ $$ = gen_import(block_const($2), $4, 1);
block_free($2);
- jv_free($4);
- jv_free(v);
} |
"import" ImportFrom "as" IDENT {
- jv v = block_const($2);
- $$ = gen_import(jv_string_value(v), jv_string_value($4), 0);
+ $$ = gen_import(block_const($2), $4, 0);
block_free($2);
- jv_free($4);
- jv_free(v);
} |
"include" ImportFrom {
- jv v = block_const($2);
- $$ = gen_import(jv_string_value(v), NULL, 0);
+ $$ = gen_import(block_const($2), jv_invalid(), 0);
block_free($2);
- jv_free(v);
}
ImportFrom:
diff --git a/tests/shtest b/tests/shtest
index 505d45d..4a5978c 100755
--- a/tests/shtest
+++ b/tests/shtest
@@ -622,4 +622,21 @@ if echo '42' | $JQ -f "$d/nul_prog.jq" >/dev/null 2>/dev/null; then
exit 1
fi
+# CVE-2026-43895: No NUL bytes in module/data import paths
+printf 'import "a\\u0000b" as $x; .' > "$d/nul_import.jq"
+if $JQ -nf "$d/nul_import.jq" >/dev/null 2>/dev/null; then
+ printf 'Error expected for import path with NUL bytes\n' 1>&2
+ exit 1
+fi
+printf 'include "a\\u0000b"; .' > "$d/nul_include.jq"
+if $JQ -nf "$d/nul_include.jq" >/dev/null 2>/dev/null; then
+ printf 'Error expected for include path with NUL bytes\n' 1>&2
+ exit 1
+fi
+printf '"a\\u0000b" | modulemeta' > "$d/nul_modulemeta.jq"
+if $JQ -nf "$d/nul_modulemeta.jq" >/dev/null 2>/dev/null; then
+ printf 'Error expected for modulemeta with NUL bytes\n' 1>&2
+ exit 1
+fi
+
exit 0
--
2.44.4
+1
View File
@@ -25,6 +25,7 @@ SRC_URI = "${GITHUB_BASE_URI}/download/${BPN}-${PV}/${BPN}-${PV}.tar.gz \
file://CVE-2026-41257.patch \
file://CVE-2026-43894.patch \
file://CVE-2026-43896.patch \
file://CVE-2026-43895.patch \
"
SRC_URI[sha256sum] = "478c9ca129fd2e3443fe27314b455e211e0d8c60bc8ff7df703873deeee580c2"