CVE-2024-28111: Insufficient Information (NVD-CWE-noinfo)
value: Canarytokens helps track activity and actions on a network. Canarytokens.org supports exporting the history of a Canarytoken's incidents in CSV format. The generation of these CSV files is vulnerable to a CSV Injection vulnerability. This flaw can be used by an attacker who discovers an HTTP-based Canarytoken to target the Canarytoken's owner, if the owner exports the incident history to CSV and opens in a reader application such as Microsoft Excel. The impact is that this issue could lead to code execution on the machine on which the CSV file is opened. Version sha-c595a1f8 contains a fix for this issue.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-03-06T22:15Z
Repository: https://github.com/thinkst/canarytokens
Language: Python
Files changed: .devcontainer/library-scripts/custom-installs.sh, .github/workflows/test.yml, aws-css-token-infra/CSSClonedSiteCFFunc/index.js, canarytokens/canarydrop.py, canarytokens/channel_dns.py
Commit date: 2024-03-06 16:16:07 +0200
Commit message: Fix CSV injection vulnerability (#355)

See https://github.com/thinkst/canarytokens/security/advisories/GHSA-fqh6-v4qp-65fv, thanks to @iodn for reporting.

(Also fixes broken build scripts for development, and the test pipeline. K8s changed repo details :/)
Diff: diff --git a/.devcontainer/library-scripts/custom-installs.sh b/.devcontainer/library-scripts/custom-installs.sh
index f2ae238..0ac348f 100644
--- a/.devcontainer/library-scripts/custom-installs.sh
+++ b/.devcontainer/library-scripts/custom-installs.sh
@@ -14,24 +14,19 @@ curl -Lo ./terraform-docs.tar.gz https://github.com/terraform-docs/terraform-doc
 tar -xzf terraform-docs.tar.gz
 chmod +x terraform-docs
 mv terraform-docs /usr/local/terraform-docs
 
 # Install mysql (default repos are broken for buster)
+apt-key adv --keyserver keyserver.ubuntu.com --recv-keys B7B3B788A8D3785C
 wget https://dev.mysql.com/get/mysql-apt-config_0.8.22-1_all.deb
 DEBIAN_FRONTEND=noninteractive dpkg -i mysql-apt-config_0.8.22-1_all.deb
 apt update
 DEBIAN_FRONTEND=noninteractive apt-get install -y mysql-client
 
 apt install -y wireguard-tools
 
-# curl -sSL https://install.python-poetry.org | POETRY_HOME=/home/vscode/.local python -
-# /home/vscode/.local/bin/poetry config virtualenvs.in-project true
-
-# wget https://golang.org/dl/go1.18.2.linux-amd64.tar.gz
-# tar -C /usr/local -xzf go1.18.2.linux-amd64.tar.gz
-# /usr/local/go/bin/go install github.com/aquasecurity/tfsec/cmd/tfsec@latest
-
-sudo curl -fsSLo /usr/share/keyrings/kubernetes-archive-keyring.gpg  https://dl.k8s.io/apt/doc/apt-key.gpg
-sudo install -o root -g root -m 644 /usr/share/keyrings/kubernetes-archive-keyring.gpg /etc/apt/trusted.gpg.d/
-sudo echo "deb [signed-by=/etc/apt/trusted.gpg.d/kubernetes-archive-keyring.gpg] https://apt.kubernetes.io kubernetes-xenial main" | sudo tee /etc/apt/sources.list.d/kubernetes.list
-sudo apt-get update -y
-sudo apt-get install -y kubectl
+mkdir -p /etc/apt/keyrings
+apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 234654DA9A296436
+curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.28/deb/Release.key | gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
+echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.28/deb/ /' | tee /etc/apt/sources.list.d/kubernetes.list
+apt-get update -y
+apt-get install -y kubectl=1.28.1-1.1
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index a925d0f..4221949 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -77,14 +77,11 @@ jobs:
           sudo apt-get install openssh-server
           curl -sSL https://install.python-poetry.org | python -
           poetry config virtualenvs.in-project true
           sudo apt-get update -y
           sudo apt-get install -y apt-transport-https ca-certificates curl
-          sudo curl -fsSLo /usr/share/keyrings/kubernetes-archive-keyring.gpg https://dl.k8s.io/apt/doc/apt-key.gpg
-          echo "deb [signed-by=/usr/share/keyrings/kubernetes-archive-keyring.gpg] https://apt.kubernetes.io/ kubernetes-xenial main" | sudo tee /etc/apt/sources.list.d/kubernetes.list
           sudo apt-get update -y
-          sudo apt-get install -y kubectl
           sudo apt-get install -y osslsigncode
           sudo apt install redis-tools
       - name: Set up cache
         uses: actions/cache@v1
         with:
diff --git a/aws-css-token-infra/CSSClonedSiteCFFunc/index.js b/aws-css-token-infra/CSSClonedSiteCFFunc/index.js
index d7e17b3..441bb98 100644
--- a/aws-css-token-infra/CSSClonedSiteCFFunc/index.js
+++ b/aws-css-token-infra/CSSClonedSiteCFFunc/index.js
@@ -48,12 +48,12 @@ function handler(event) {
     if (expected_referrer == '')
         console.log("Empty expected_referrer!");
     if (referer == '')
         console.log("Empty/missing Referer header for: " + expected_referrer);
 
-    if (expected_referrer == '' || referer == '' || referer_origin.endsWith(expected_referrer) || referer_origin.endsWith(event.context.distributionDomainName)) { 
-        // Happy case where the referer matches   
+    if (expected_referrer == '' || referer == '' || referer_origin.endsWith(expected_referrer) || referer_origin.endsWith(event.context.distributionDomainName)) {
+        // Happy case where the referer matches
         return matching_ref_response;
     }
     if (expected_referrer.endsWith('microsoftonline.com') && referer_origin.endsWith('login.microsoft.com')) {
         // Special case of an MS login token came from login.microsoft.com instead of microsoftonline.com
         // We still want to treat this as a good login since the referer is a valid MS domain
diff --git a/canarytokens/canarydrop.py b/canarytokens/canarydrop.py
index cc52e36..9f0a931 100644
--- a/canarytokens/canarydrop.py
+++ b/canarytokens/canarydrop.py
@@ -455,12 +455,16 @@ class Canarydrop(BaseModel):
 
     def alerting(self) -> None:
         self.user.do_accounting(canarydrop=self)
 
     def get_csv_incident_list(self) -> str:
+        def escape_csv_field(data) -> str:
+            data = f"'{data}"
+            return data
+
         csvOutput = io.StringIO()
-        writer = csv.writer(csvOutput)
+        writer = csv.writer(csvOutput, quoting=csv.QUOTE_ALL)
 
         if len(self.triggered_details.hits) > 0:  # pragma: no cover
             hit_class_dict = dict(self.triggered_details.hits[0])
             headers = [
                 (i)
@@ -474,11 +478,16 @@ class Canarydrop(BaseModel):
                     "%Y-%m-%d %H:%M:%S.%f"
                 )
                 hit_dict = dict(hit)
                 data = [hit_id]
                 for key in headers:
-                    data.append(hit_dict.get(key, "N/A"))
+                    csv_field = hit_dict.get(key, "N/A")
+
+                    # The row includeds non-str objects, but they are all passed through __str__() by CSV writer,
+                    # so we sanitise those and add strings only to the row.
+                    csv_field = escape_csv_field(csv_field.__str__())
+                    data.append(csv_field)
                 writer.writerow(data)
         else:
             writer.writerow("the token has not been triggered")
 
         return csvOutput.getvalue()
diff --git a/canarytokens/channel_dns.py b/canarytokens/channel_dns.py
index 1f23aac..6dfe0d5 100644
--- a/canarytokens/channel_dns.py
+++ b/canarytokens/channel_dns.py
@@ -110,11 +110,11 @@ class ChannelDNS(InputChannel):
                 ttl=300,
                 name=".".join(["ns1", name.decode()]),
             ),
             type=dns.NS,
             auth=True,
-            ttl=300
+            ttl=300,
         )
         additional = dns.RRHeader(
             name=".".join(["ns1", name.decode()]),
             payload=dns.Record_A(ttl=10, address=self.frontend_settings.PUBLIC_IP),
             type=dns.A,

Vulnerable code: # curl -sSL https://install.python-poetry.org | POETRY_HOME=/home/vscode/.local python -
# /home/vscode/.local/bin/poetry config virtualenvs.in-project true

# wget https://golang.org/dl/go1.18.2.linux-amd64.tar.gz
# tar -C /usr/local -xzf go1.18.2.linux-amd64.tar.gz
# /usr/local/go/bin/go install github.com/aquasecurity/tfsec/cmd/tfsec@latest

sudo curl -fsSLo /usr/share/keyrings/kubernetes-archive-keyring.gpg  https://dl.k8s.io/apt/doc/apt-key.gpg
sudo install -o root -g root -m 644 /usr/share/keyrings/kubernetes-archive-keyring.gpg /etc/apt/trusted.gpg.d/
sudo echo "deb [signed-by=/etc/apt/trusted.gpg.d/kubernetes-archive-keyring.gpg] https://apt.kubernetes.io kubernetes-xenial main" | sudo tee /etc/apt/sources.list.d/kubernetes.list
sudo apt-get update -y
sudo apt-get install -y kubectl
          sudo curl -fsSLo /usr/share/keyrings/kubernetes-archive-keyring.gpg https://dl.k8s.io/apt/doc/apt-key.gpg
          echo "deb [signed-by=/usr/share/keyrings/kubernetes-archive-keyring.gpg] https://apt.kubernetes.io/ kubernetes-xenial main" | sudo tee /etc/apt/sources.list.d/kubernetes.list
          sudo apt-get install -y kubectl
    if (expected_referrer == '' || referer == '' || referer_origin.endsWith(expected_referrer) || referer_origin.endsWith(event.context.distributionDomainName)) { 
        // Happy case where the referer matches   
        writer = csv.writer(csvOutput)
                    data.append(hit_dict.get(key, "N/A"))
            ttl=300
Fixed code: apt-key adv --keyserver keyserver.ubuntu.com --recv-keys B7B3B788A8D3785C
mkdir -p /etc/apt/keyrings
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 234654DA9A296436
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.28/deb/Release.key | gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.28/deb/ /' | tee /etc/apt/sources.list.d/kubernetes.list
apt-get update -y
apt-get install -y kubectl=1.28.1-1.1
    if (expected_referrer == '' || referer == '' || referer_origin.endsWith(expected_referrer) || referer_origin.endsWith(event.context.distributionDomainName)) {
        // Happy case where the referer matches
        def escape_csv_field(data) -> str:
            data = f"'{data}"
            return data

        writer = csv.writer(csvOutput, quoting=csv.QUOTE_ALL)
                    csv_field = hit_dict.get(key, "N/A")

                    # The row includeds non-str objects, but they are all passed through __str__() by CSV writer,
                    # so we sanitise those and add strings only to the row.
                    csv_field = escape_csv_field(csv_field.__str__())
                    data.append(csv_field)
            ttl=300,