This is an automated email from the ASF dual-hosted git repository.

vatsrahul1001 pushed a commit to branch v3-4-test
in repository https://gitbox.apache.org/repos/asf/airflow.git

commit c7f0d7910677e02d876dcc12e0e2ceed24cdb25f
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Fri Oct 30 12:14:35 2026 +0530

    [v3-4-test] Reject non-object JSON files when importing variables (#75384) 
(#75950)
    
    Array and primitive documents cannot represent a mapping of variable names 
to values and can otherwise enable unintended imports.
    (cherry picked from commit eafb6ed8a7d834d52527ddcc601c5b90e9c8cde0)
    
    Co-authored-by: Himanshu Chawla <[email protected]>
    Co-authored-by: Himanshu Chawla 
<[email protected]>
---
 .../pages/Variables/ImportVariablesForm.test.tsx   | 83 ++++++++++++++++++++++
 .../ui/src/pages/Variables/ImportVariablesForm.tsx | 12 +++-
 2 files changed, 92 insertions(+), 3 deletions(-)

diff --git 
a/airflow-core/src/airflow/ui/src/pages/Variables/ImportVariablesForm.test.tsx 
b/airflow-core/src/airflow/ui/src/pages/Variables/ImportVariablesForm.test.tsx
new file mode 100644
index 00000000000..f4d76a046b0
--- /dev/null
+++ 
b/airflow-core/src/airflow/ui/src/pages/Variables/ImportVariablesForm.test.tsx
@@ -0,0 +1,83 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import "@testing-library/jest-dom/vitest";
+import { fireEvent, render, screen, waitFor } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
+
+import { Wrapper } from "src/utils/Wrapper";
+
+import ImportVariablesForm from "./ImportVariablesForm";
+
+const { mutate, setError } = vi.hoisted(() => ({ mutate: vi.fn(), setError: 
vi.fn() }));
+
+vi.mock("src/queries/useImportVariables", () => ({
+  useImportVariables: () => ({ error: undefined, isPending: false, mutate, 
setError }),
+}));
+
+const uploadVariables = (content: string) => {
+  const { container } = render(<ImportVariablesForm onClose={vi.fn()} />, { 
wrapper: Wrapper });
+  const input = container.querySelector("input[type=file]");
+
+  if (input === null) {
+    throw new Error("File input not found");
+  }
+  fireEvent.input(input, {
+    target: { files: [new File([content], "variables.json", { type: 
"application/json" })] },
+  });
+};
+
+describe("ImportVariablesForm", () => {
+  it.each(['["example"]', '"example"', "1", "true", "null"])(
+    "rejects a non-object JSON document: %s",
+    async (content) => {
+      uploadVariables(content);
+
+      await waitFor(() =>
+        expect(setError).toHaveBeenCalledWith({
+          body: { detail: "variables.import.errorParsingJsonFile" },
+        }),
+      );
+      expect(screen.getByRole("button", { name: "variables.import.button" 
})).toBeDisabled();
+      expect(mutate).not.toHaveBeenCalled();
+    },
+  );
+
+  it("imports a dictionary without changing its variable values", async () => {
+    uploadVariables('{"name":"example","config":{"enabled":true}}');
+    const button = screen.getByRole("button", { name: 
"variables.import.button" });
+
+    await waitFor(() => expect(button).toBeEnabled());
+    fireEvent.click(button);
+
+    expect(mutate).toHaveBeenCalledWith({
+      requestBody: {
+        actions: [
+          {
+            action: "create",
+            action_on_existence: "fail",
+            entities: [
+              { key: "name", value: "example" },
+              { key: "config", value: { enabled: true } },
+            ],
+          },
+        ],
+      },
+    });
+  });
+});
diff --git 
a/airflow-core/src/airflow/ui/src/pages/Variables/ImportVariablesForm.tsx 
b/airflow-core/src/airflow/ui/src/pages/Variables/ImportVariablesForm.tsx
index abaa8393425..ffd5b65b53d 100644
--- a/airflow-core/src/airflow/ui/src/pages/Variables/ImportVariablesForm.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Variables/ImportVariablesForm.tsx
@@ -78,17 +78,23 @@ const ImportVariablesForm = ({ onClose }: 
ImportVariablesFormProps) => {
     reader.addEventListener("load", (event) => {
       const text = event.target?.result as string;
 
-      try {
-        const parsedContent = JSON.parse(text) as Record<string, unknown>;
+      let parsedContent: unknown;
 
-        setFileContent(parsedContent);
+      try {
+        parsedContent = JSON.parse(text);
       } catch {
+        parsedContent = undefined;
+      }
+
+      if (parsedContent === null || typeof parsedContent !== "object" || 
Array.isArray(parsedContent)) {
         setError({
           body: {
             detail: translate("variables.import.errorParsingJsonFile"),
           },
         });
         setFileContent(undefined);
+      } else {
+        setFileContent(parsedContent as Record<string, unknown>);
       }
 
       setIsParsing(false);

Reply via email to