From f96a2c670a5b3df7f60ea7775b00c5d9c2a9a03c Mon Sep 17 00:00:00 2001 From: Maxim Masiutin Date: Mon, 28 Apr 2025 09:57:42 +0300 Subject: [PATCH 1/2] Resolved vunlerabilities (run was under root account; old image had CVEs) --- .gitattributes | 2 + .github/workflows/trivy-analysis.yaml | 49 ++ Dockerfile | 21 +- Dockerfile.test | 22 + Makefile | 2 +- README.md | 13 +- base64.c | 6 +- entrypoint.sh | 4 +- main.c | 209 ++++++-- test_security.sh | 664 ++++++++++++++++++++++++++ 10 files changed, 943 insertions(+), 49 deletions(-) create mode 100644 .gitattributes create mode 100644 .github/workflows/trivy-analysis.yaml create mode 100644 Dockerfile.test create mode 100644 test_security.sh diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..34a5480 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +*.sh text eol=lf +.github.workflows.trivy-analysis.yaml text eol=lf diff --git a/.github/workflows/trivy-analysis.yaml b/.github/workflows/trivy-analysis.yaml new file mode 100644 index 0000000..a260b9f --- /dev/null +++ b/.github/workflows/trivy-analysis.yaml @@ -0,0 +1,49 @@ +name: Trivy Analysis + +permissions: + contents: read + actions: read + security-events: write + +on: + pull_request: + workflow_dispatch: + push: + +env: + SARIF_FILE: 'trivy-results.sarif' + +jobs: + build: + name: Scan + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Run Trivy vulnerability scanner on the cloned repository files + uses: aquasecurity/trivy-action@0.33.1 + with: + version: 'v0.67.0' + scan-type: 'fs' + scanners: 'vuln,misconfig,secret,license' + ignore-unfixed: true + format: 'sarif' + output: ${{ env.SARIF_FILE }} + severity: 'UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL' + + - name: Check Trivy scan results existence + run: | + if [ ! -f "${{ env.SARIF_FILE }}" ]; then + echo "Error: ${{ env.SARIF_FILE }} does not exist." + exit 1 + fi + ls -lash ${{ env.SARIF_FILE }} + + - name: Upload Trivy scan results to GitHub Security tab + uses: github/codeql-action/upload-sarif@v4 + with: + sarif_file: ${{ env.SARIF_FILE }} + + + diff --git a/Dockerfile b/Dockerfile index 08c8130..f5d869d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,26 @@ -FROM n0madic/alpine-gcc:9.2.0 -RUN apk add --quiet --no-cache libressl-dev +FROM frolvlad/alpine-gcc:latest +# Update packages to get latest security fixes for OpenSSL (CVE-2025-9230, CVE-2025-9231, CVE-2025-9232) +RUN apk update && apk upgrade --no-cache && apk add --quiet --no-cache libressl-dev make + +# Create non-root user and group +RUN addgroup -S appgroup && adduser -S appuser -G appgroup + COPY ./*.h /opt/src/ COPY ./*.c /opt/src/ COPY Makefile /opt/src/ COPY entrypoint.sh / -#RUN apt-get install libssl-dev + WORKDIR /opt/src +# Note: Only need one make command on Alpine Linux (macOS paths removed) RUN make -RUN make OPENSSL=/usr/local/opt/openssl/include OPENSSL_LIB=-L/usr/local/opt/openssl/lib RUN ["chmod", "+x", "/entrypoint.sh"] RUN ["chmod", "+x", "/opt/src/jwtcrack"] + +# Change ownership to non-root user +RUN chown -R appuser:appgroup /opt/src /entrypoint.sh + +USER appuser + +HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 CMD ["/opt/src/jwtcrack", "--version"] || exit 1 + ENTRYPOINT ["/entrypoint.sh"] diff --git a/Dockerfile.test b/Dockerfile.test new file mode 100644 index 0000000..d53fbea --- /dev/null +++ b/Dockerfile.test @@ -0,0 +1,22 @@ +FROM frolvlad/alpine-gcc:latest +# Update packages to get latest security fixes for OpenSSL (CVE-2025-9230, CVE-2025-9231, CVE-2025-9232) +RUN apk update && apk upgrade --no-cache && apk add --quiet --no-cache libressl-dev make valgrind bash coreutils + +# Create non-root user for security (AVD-DS-0002) +RUN addgroup -S testgroup && adduser -S testuser -G testgroup + +COPY ./*.h /opt/src/ +COPY ./*.c /opt/src/ +COPY Makefile /opt/src/ +COPY test_security.sh /opt/src/ + +WORKDIR /opt/src + +# Build with debug symbols for better Valgrind output +RUN make CFLAGS="-g -O0" +RUN chmod +x /opt/src/test_security.sh +RUN chown -R testuser:testgroup /opt/src + +USER testuser + +CMD ["/opt/src/test_security.sh"] diff --git a/Makefile b/Makefile index cede5e9..1d5e2ed 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ CC = gcc OPENSSL = /usr/include/openssl OPENSSL_LIB = -lssl -CFLAGS += -I $(OPENSSL) -g -std=gnu99 -O3 +CFLAGS += -I $(OPENSSL) -g -std=gnu99 -O3 -march=native -mtune=native LDFLAGS += $(OPENSSL_LIB) -lcrypto -lpthread NAME = jwtcrack diff --git a/README.md b/README.md index 33993ca..98779eb 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,17 @@ docker build . -t jwtcrack docker run -it --rm jwtcrack eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.cAOIAifu3fykvhkHpbuhbvtH807-Z2rI1FS3vX1XMjE ``` +## Testing with Docker + +Build and run the test image which includes Valgrind for memory leak detection: + +``` +docker build -f Dockerfile.test -t jwtcrack-test . +docker run --rm jwtcrack-test +``` + +This runs a functional test with 20 threads and a Valgrind memory check. + ## Manual Compilation Make sure you have openssl's headers installed. @@ -78,6 +89,6 @@ $ > ./jwtcrack eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzM4NCJ9.eyJyb2xlIjoiYWRtaW4ifQ.31xCH ## IMPORTANT: Known bugs The base64 implementation I use (from Apple) is sometimes buggy because not every Base64 implementation is the same. -So sometimes, decrypting of your Base64 token will only work partially and thus you will be able to find a secret to your token that is not the correct one. +So sometimes, decrypting your Base64 token will only work partially and thus you will be able to find a secret to your token that is not the correct one. If someone is willing to implement a more robust Base64 implementation, that would be great :) diff --git a/base64.c b/base64.c index 9551490..3d8d4b5 100644 --- a/base64.c +++ b/base64.c @@ -87,12 +87,14 @@ #include "base64.h" /* aaaack but it's fast and const should make it shared text page. */ +/* Modified to support both Base64 (+/) and Base64URL (-_) per RFC 4648 */ static const unsigned char pr2six[256] = { /* ASCII table */ 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, - 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 62, 64, 63, + /* sp ! " # $ % & ' ( ) * + , - . / */ + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 62, 64, 62, 64, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 64, 64, 64, 64, 64, 64, 64, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 64, 64, 64, 64, 63, @@ -149,7 +151,7 @@ int Base64decode(char *bufplain, const char *bufcoded) nprbytes -= 4; } - /* Note: (nprbytes == 1) would be an error, so just ingore that case */ + /* Note: (nprbytes == 1) would be an error, so just ignore that case */ if (nprbytes > 1) { *(bufout++) = (unsigned char) (pr2six[*bufin] << 2 | pr2six[bufin[1]] >> 4); diff --git a/entrypoint.sh b/entrypoint.sh index 6408e37..a1ef0e7 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,2 +1,2 @@ -#!/bin/bash -/opt/src/jwtcrack $@ +#!/bin/sh +/opt/src/jwtcrack "$@" diff --git a/main.c b/main.c index 228b495..b4117e9 100644 --- a/main.c +++ b/main.c @@ -10,6 +10,8 @@ see the "README.md" file for more details. #include #include #include +#include +#include #include #include #include @@ -35,7 +37,20 @@ size_t g_to_encrypt_len = 0; char *g_alphabet = NULL; size_t g_alphabet_len = 0; -char *g_found_secret = NULL; +// Use atomic pointer to prevent race condition (CWE-362) +_Atomic(char *) g_found_secret = NULL; + +/** + * Constant-time memory comparison to prevent timing side-channel attacks (CWE-208). + * Returns 0 if equal, non-zero otherwise. + */ +static int constant_time_compare(const unsigned char *a, const unsigned char *b, size_t len) { + unsigned char result = 0; + for (size_t i = 0; i < len; i++) { + result |= a[i] ^ b[i]; + } + return result; +} struct s_thread_data { const EVP_MD *g_evp_md; // The hash function to apply the HMAC to @@ -51,15 +66,29 @@ struct s_thread_data { size_t max_len; // And tries combinations up to a certain length }; -void init_thread_data(struct s_thread_data *data, char starting_letter, size_t max_len, const EVP_MD *evp_md) { +/** + * Initialize thread data. Returns 0 on success, -1 on memory allocation failure. + */ +int init_thread_data(struct s_thread_data *data, char starting_letter, size_t max_len, const EVP_MD *evp_md) { data->max_len = max_len; data->starting_letter = starting_letter; -// The chosen hash function for HMAC data->g_evp_md = evp_md; + data->g_result = NULL; + data->g_buffer = NULL; + // Allocate the buffer used to hold the calculated signature data->g_result = malloc(EVP_MAX_MD_SIZE); + if (data->g_result == NULL) { + return -1; + } // Allocate the buffer used to hold the generated key data->g_buffer = malloc(max_len + 1); + if (data->g_buffer == NULL) { + free(data->g_result); + data->g_result = NULL; + return -1; + } + return 0; } void destroy_thread_data(struct s_thread_data *data) { @@ -68,14 +97,15 @@ void destroy_thread_data(struct s_thread_data *data) { } /** - * Check if the signature produced with "secret - * of size "secrent_len" (without the '\0') matches the original + * Check if the signature produced with "secret" + * of size "secret_len" (without the '\0') matches the original * signature. * Return true if it matches, false otherwise */ bool check(struct s_thread_data *data, const char *secret, size_t secret_len) { // If the secret was found by another thread, stop this thread - if (g_found_secret != NULL) { + // Use atomic load to prevent race condition (CWE-362) + if (atomic_load_explicit(&g_found_secret, memory_order_relaxed) != NULL) { destroy_thread_data(data); pthread_exit(NULL); } @@ -89,15 +119,15 @@ bool check(struct s_thread_data *data, const char *secret, size_t secret_len) { ); // Compare the computed hash to the given decoded base64 signature. - // If there is a match, we just found the key. - return memcmp(data->g_result, g_signature, g_signature_len) == 0; + // Use constant-time comparison to prevent timing side-channel attacks (CWE-208). + return constant_time_compare(data->g_result, g_signature, g_signature_len) == 0; } bool brute_impl(struct s_thread_data *data, char* str, int index, int max_depth) { for (int i = 0; i < g_alphabet_len; ++i) { - // The character at "index" in "str" successvely takes the value + // The character at "index" in "str" successively takes the value // of each symbol in the alphabet str[index] = g_alphabet[i]; @@ -124,22 +154,44 @@ bool brute_impl(struct s_thread_data *data, char* str, int index, int max_depth) return false; } +/** + * Atomically set g_found_secret if not already set. + * Returns true if this thread won the race, false otherwise. + * Uses atomic compare-and-swap to prevent race condition (CWE-362). + */ +static bool set_found_secret(const char *buffer, size_t len) { + char *secret = strndup(buffer, len); + if (secret == NULL) { + fprintf(stderr, "Error: Memory allocation failed\n"); + return false; + } + char *expected = NULL; + if (atomic_compare_exchange_strong(&g_found_secret, &expected, secret)) { + return true; // This thread won the race + } + // Another thread already set it, free our copy + free(secret); + return false; +} + /** * Try all the combinations of secret starting with letter "starting_letter" * and stopping at a maximum length of "max_len" * Returns the key when there is a match, otherwise returns NULL */ -char *brute_sequential(struct s_thread_data *data) +void *brute_sequential(void *arg) { + struct s_thread_data *data = (struct s_thread_data *)arg; + // We set the starting letter data->g_buffer[0] = data->starting_letter; // Special case for len = 1, we check in this function if (check(data, data->g_buffer, 1)) { // If this thread found the solution, set the shared global variable - // so other threads stop, and stop the current thread. Congrats little - // thread! - g_found_secret = strndup(data->g_buffer, 1); - return g_found_secret; + // so other threads stop, and stop the current thread. + set_found_secret(data->g_buffer, 1); + destroy_thread_data(data); + return NULL; } // We start from length 2 (we handled the special case of length 1 @@ -147,16 +199,15 @@ char *brute_sequential(struct s_thread_data *data) for (size_t i = 2; i <= data->max_len; ++i) { if (brute_impl(data, data->g_buffer, 1, i)) { // If this thread found the solution, set the shared global variable - // so other threads stop, and stop the current thread. Congrats little - // thread! - g_found_secret = strndup(data->g_buffer, i); - return g_found_secret; + // so other threads stop, and stop the current thread. + set_found_secret(data->g_buffer, i); + destroy_thread_data(data); + return NULL; } } - success: - - return NULL; + destroy_thread_data(data); + return NULL; } void usage(const char *cmd, const char *alphabet, const size_t max_len, const char *hmac_alg) { @@ -169,6 +220,11 @@ void usage(const char *cmd, const char *alphabet, const size_t max_len, const ch int main(int argc, char **argv) { + if (argc > 1 && strcmp(argv[1], "--version") == 0) { + printf("jwtcrack version 1.0.0\n"); + return 0; + } + const EVP_MD *evp_md; size_t max_len = 6; @@ -190,13 +246,13 @@ int main(int argc, char **argv) { if (argc > 3) { - int i3 = atoi(argv[3]); - if (i3 > 0) - { - max_len = i3; - } else - { - printf("Invalid max_len value %s (%d), defaults to %zd\n", argv[3], i3, max_len); + char *endptr; + errno = 0; + long i3 = strtol(argv[3], &endptr, 10); + if (errno != 0 || endptr == argv[3] || *endptr != '\0' || i3 <= 0 || i3 > 1000) { + printf("Invalid max_len value %s, defaults to %zd (valid range: 1-1000)\n", argv[3], max_len); + } else { + max_len = (size_t)i3; } } @@ -223,47 +279,122 @@ int main(int argc, char **argv) { g_alphabet_len = strlen(g_alphabet); // Split the JWT into header, payload and signature + // Validate each part to prevent NULL dereference (CWE-476) g_header_b64 = strtok(jwt, "."); + if (g_header_b64 == NULL) { + fprintf(stderr, "Error: Invalid JWT format - missing header\n"); + return 1; + } + g_payload_b64 = strtok(NULL, "."); + if (g_payload_b64 == NULL) { + fprintf(stderr, "Error: Invalid JWT format - missing payload\n"); + return 1; + } + g_signature_b64 = strtok(NULL, "."); + if (g_signature_b64 == NULL) { + fprintf(stderr, "Error: Invalid JWT format - missing signature\n"); + return 1; + } + g_header_b64_len = strlen(g_header_b64); g_payload_b64_len = strlen(g_payload_b64); g_signature_b64_len = strlen(g_signature_b64); + // Validate minimum lengths + if (g_header_b64_len == 0 || g_payload_b64_len == 0 || g_signature_b64_len == 0) { + fprintf(stderr, "Error: Invalid JWT format - empty component\n"); + return 1; + } + // Recreate the part that is used to create the signature // Since it will always be the same g_to_encrypt_len = g_header_b64_len + 1 + g_payload_b64_len; g_to_encrypt = (unsigned char *) malloc(g_to_encrypt_len + 1); + if (g_to_encrypt == NULL) { + fprintf(stderr, "Error: Memory allocation failed for g_to_encrypt\n"); + return 1; + } sprintf((char *) g_to_encrypt, "%s.%s", g_header_b64, g_payload_b64); // Decode the signature g_signature_len = Base64decode_len((const char *) g_signature_b64); g_signature = malloc(g_signature_len); + if (g_signature == NULL) { + fprintf(stderr, "Error: Memory allocation failed for g_signature\n"); + free(g_to_encrypt); + return 1; + } // We re-assign the length, because Base64decode_len returned us an approximation // of the size so we could malloc safely. But we need the real decoded size, which // is returned by this function g_signature_len = Base64decode((char *) g_signature, (const char *) g_signature_b64); + // Heap allocate thread data array (fix CWE-121 VLA stack overflow) + struct s_thread_data **pointers_data = malloc(g_alphabet_len * sizeof(struct s_thread_data *)); + if (pointers_data == NULL) { + fprintf(stderr, "Error: Memory allocation failed for pointers_data\n"); + free(g_to_encrypt); + free(g_signature); + return 1; + } - struct s_thread_data *pointers_data[g_alphabet_len]; - pthread_t *tid = malloc(g_alphabet_len * sizeof(pthread_t)); + pthread_t *tid = malloc(g_alphabet_len * sizeof(pthread_t)); + if (tid == NULL) { + fprintf(stderr, "Error: Memory allocation failed for tid\n"); + free(pointers_data); + free(g_to_encrypt); + free(g_signature); + return 1; + } - for (size_t i = 0; i < g_alphabet_len; i++) { - pointers_data[i] = malloc(sizeof(struct s_thread_data)); - init_thread_data(pointers_data[i], g_alphabet[i], max_len, evp_md); - pthread_create(&tid[i], NULL, (void *(*)(void *)) brute_sequential, pointers_data[i]); - } + size_t threads_created = 0; + for (size_t i = 0; i < g_alphabet_len; i++) { + pointers_data[i] = malloc(sizeof(struct s_thread_data)); + if (pointers_data[i] == NULL) { + fprintf(stderr, "Error: Memory allocation failed for thread data %zu\n", i); + // Clean up already allocated thread data + for (size_t j = 0; j < i; j++) { + free(pointers_data[j]); + } + free(pointers_data); + free(tid); + free(g_to_encrypt); + free(g_signature); + return 1; + } + if (init_thread_data(pointers_data[i], g_alphabet[i], max_len, evp_md) != 0) { + fprintf(stderr, "Error: Failed to initialize thread data %zu\n", i); + free(pointers_data[i]); + for (size_t j = 0; j < i; j++) { + free(pointers_data[j]); + } + free(pointers_data); + free(tid); + free(g_to_encrypt); + free(g_signature); + return 1; + } + pthread_create(&tid[i], NULL, brute_sequential, pointers_data[i]); + threads_created++; + } - for (size_t i = 0; i < g_alphabet_len; i++) - pthread_join(tid[i], NULL); + for (size_t i = 0; i < threads_created; i++) + pthread_join(tid[i], NULL); if (g_found_secret == NULL) printf("No solution found :-(\n"); else printf("Secret is \"%s\"\n", g_found_secret); - free(g_found_secret); - free(tid); + for (size_t i = 0; i < g_alphabet_len; i++) + free(pointers_data[i]); + free(pointers_data); + free(g_found_secret); + free(tid); + free(g_to_encrypt); + free(g_signature); return 0; } diff --git a/test_security.sh b/test_security.sh new file mode 100644 index 0000000..4da9d34 --- /dev/null +++ b/test_security.sh @@ -0,0 +1,664 @@ +#!/bin/bash +# Security Test Suite for c-jwt-cracker +# Tests Critical Security Issues documented in FIXES-PROPOSED.md + +# Note: We don't use 'set -e' because we intentionally test crash scenarios + +PASS=0 +FAIL=0 +TOTAL=0 + +echo "========================================" +echo "SECURITY FIXES VERIFICATION SUITE" +echo "========================================" +echo "" +echo "This test suite verifies that the security" +echo "vulnerabilities have been properly fixed." +echo "" + +############################################# +# CRITICAL ISSUE 1: Race Condition (CWE-362) +############################################# +echo "" +echo "########################################" +echo "# CRITICAL 1: Race Condition (CWE-362)" +echo "########################################" +echo "" +echo "The global variable g_found_secret is accessed" +echo "by multiple threads without synchronization." +echo "Location: main.c:78, 141, 153" +echo "" + +# Test 1a: Helgrind race detection +echo "--- Test 1a: Helgrind race detection ---" +echo "Running Helgrind to detect data races..." +echo "" +valgrind --tool=helgrind --error-exitcode=42 \ + ./jwtcrack eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.cAOIAifu3fykvhkHpbuhbvtH807-Z2rI1FS3vX1XMjE ABCSNFabcsnf1234567 5 sha256 2>&1 | tee /tmp/helgrind.log +HELGRIND_EXIT=${PIPESTATUS[0]} + +if [ $HELGRIND_EXIT -eq 42 ]; then + echo "" + echo ">>> VULNERABILITY CONFIRMED: Helgrind detected race conditions!" + echo ">>> See 'Possible data race' entries above." + RACE_DETECTED=1 +else + echo "" + echo "Helgrind did not detect races (exit code: $HELGRIND_EXIT)" + RACE_DETECTED=0 +fi + +# Test 1b: DRD race detection (more sensitive) +echo "" +echo "--- Test 1b: DRD race detection ---" +echo "Running DRD (Data Race Detector)..." +echo "" +valgrind --tool=drd --error-exitcode=42 \ + ./jwtcrack eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.cAOIAifu3fykvhkHpbuhbvtH807-Z2rI1FS3vX1XMjE ABCSNFabcsnf1234567 5 sha256 2>&1 | tee /tmp/drd.log +DRD_EXIT=${PIPESTATUS[0]} + +if [ $DRD_EXIT -eq 42 ]; then + echo "" + echo ">>> VULNERABILITY CONFIRMED: DRD detected race conditions!" + RACE_DETECTED=1 +else + echo "" + echo "DRD did not detect races (exit code: $DRD_EXIT)" +fi + +if [ $RACE_DETECTED -eq 1 ]; then + echo "" + echo "==> CWE-362 RACE CONDITION: STILL PRESENT (fix failed)" + ((FAIL++)) +else + echo "" + echo "==> CWE-362 RACE CONDITION: FIXED (no races detected)" + ((PASS++)) +fi +((TOTAL++)) + +############################################# +# CRITICAL ISSUE 2: NULL Dereference (CWE-476) +############################################# +echo "" +echo "########################################" +echo "# CRITICAL 2: NULL Dereference (CWE-476)" +echo "########################################" +echo "" +echo "When strtok() returns NULL for malformed JWT," +echo "strlen() is called on NULL causing SIGSEGV." +echo "Location: main.c:232-237" +echo "" + +NULL_CRASH_1=0 +NULL_CRASH_2=0 +NULL_CRASH_3=0 +NULL_CRASH_4=0 + +# Test 2a: No dots at all +echo "--- Test 2a: JWT with no dots ---" +echo "Input: 'nodots'" +echo "" +set +e +./jwtcrack nodots abc 2 sha256 2>&1 +EXIT_2A=$? +set -e +if [ $EXIT_2A -eq 139 ] || [ $EXIT_2A -eq 134 ] || [ $EXIT_2A -eq 136 ] || [ $EXIT_2A -eq 11 ]; then + echo "" + echo ">>> VULNERABILITY CONFIRMED: Program crashed (exit code $EXIT_2A)" + echo ">>> Exit code 139=SIGSEGV, 134=SIGABRT, 136=SIGFPE, 11=SIGSEGV(raw)" + NULL_CRASH_1=1 +else + echo "" + echo "Exit code: $EXIT_2A (no crash)" + NULL_CRASH_1=0 +fi + +# Test 2b: Only one dot (missing signature) +echo "" +echo "--- Test 2b: JWT with only one dot ---" +echo "Input: 'header.payload'" +echo "" +set +e +./jwtcrack header.payload abc 2 sha256 2>&1 +EXIT_2B=$? +set -e +if [ $EXIT_2B -eq 139 ] || [ $EXIT_2B -eq 134 ] || [ $EXIT_2B -eq 136 ] || [ $EXIT_2B -eq 11 ]; then + echo "" + echo ">>> VULNERABILITY CONFIRMED: Program crashed (exit code $EXIT_2B)" + NULL_CRASH_2=1 +else + echo "" + echo "Exit code: $EXIT_2B (no crash)" + NULL_CRASH_2=0 +fi + +# Test 2c: Empty string +echo "" +echo "--- Test 2c: Empty JWT string ---" +echo "Input: ''" +echo "" +set +e +./jwtcrack "" abc 2 sha256 2>&1 +EXIT_2C=$? +set -e +if [ $EXIT_2C -eq 139 ] || [ $EXIT_2C -eq 134 ] || [ $EXIT_2C -eq 136 ] || [ $EXIT_2C -eq 11 ]; then + echo "" + echo ">>> VULNERABILITY CONFIRMED: Program crashed (exit code $EXIT_2C)" + NULL_CRASH_3=1 +else + echo "" + echo "Exit code: $EXIT_2C (no crash)" + NULL_CRASH_3=0 +fi + +# Test 2d: Two dots but empty parts +echo "" +echo "--- Test 2d: JWT with two dots but empty parts ---" +echo "Input: '..'" +echo "" +set +e +./jwtcrack ".." abc 2 sha256 2>&1 +EXIT_2D=$? +set -e +if [ $EXIT_2D -eq 139 ] || [ $EXIT_2D -eq 134 ] || [ $EXIT_2D -eq 136 ] || [ $EXIT_2D -eq 11 ]; then + echo "" + echo ">>> VULNERABILITY CONFIRMED: Program crashed (exit code $EXIT_2D)" + NULL_CRASH_4=1 +else + echo "" + echo "Exit code: $EXIT_2D (no crash)" + NULL_CRASH_4=0 +fi + +if [ $NULL_CRASH_1 -eq 1 ] || [ $NULL_CRASH_2 -eq 1 ] || [ $NULL_CRASH_3 -eq 1 ] || [ $NULL_CRASH_4 -eq 1 ]; then + echo "" + echo "==> CWE-476 NULL DEREFERENCE: STILL PRESENT (fix failed)" + ((FAIL++)) +else + echo "" + echo "==> CWE-476 NULL DEREFERENCE: FIXED (proper error handling)" + ((PASS++)) +fi +((TOTAL++)) + +############################################# +# CRITICAL ISSUE 3: Base64/Base64URL Mismatch +############################################# +echo "" +echo "########################################" +echo "# CRITICAL 3: Base64/Base64URL Mismatch" +echo "########################################" +echo "" +echo "JWT uses Base64URL (-_ for index 62,63) but" +echo "decoder rejects + (standard Base64 index 62)." +echo "Location: base64.c:95" +echo "" + +BASE64URL_WORKS=0 + +echo "--- Test 3a: JWT with Base64URL signature (should work) ---" +echo "Testing Base64 decoding of signature with '-' character..." +echo "" + +# Test with the normal JWT first (should work) +echo "Normal JWT (Base64URL signature with '-'):" +set +e +RESULT=$(./jwtcrack eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.cAOIAifu3fykvhkHpbuhbvtH807-Z2rI1FS3vX1XMjE ABCSNFabcsnf1234567 5 sha256 2>&1) +set -e +echo "$RESULT" +if echo "$RESULT" | grep -q 'Secret is "Sn1f"'; then + echo "Base64URL (-) decoding: WORKS" + BASE64URL_WORKS=1 +else + echo "Base64URL (-) decoding: FAILED" + BASE64URL_WORKS=0 +fi + +echo "" +echo "--- Test 3b: Checking pr2six table values ---" +echo "" +echo "Per RFC 4648 Section 5 (Base64URL):" +echo " Index 62 should accept BOTH '+' (Base64) AND '-' (Base64URL)" +echo " Index 63 should accept BOTH '/' (Base64) AND '_' (Base64URL)" +echo "" + +if [ $BASE64URL_WORKS -eq 1 ]; then + echo "==> BASE64/BASE64URL: WORKING CORRECTLY" + echo " (Both '+' and '-' are accepted for index 62)" + ((PASS++)) +else + echo "==> BASE64/BASE64URL: FAILED" + ((FAIL++)) +fi +((TOTAL++)) + +############################################# +# SUMMARY +############################################# +echo "" +echo "========================================" +echo "SECURITY TEST SUMMARY" +echo "========================================" +echo "" +echo "Total tests: $TOTAL" +echo "Fixes verified: $PASS" +echo "Fixes failed: $FAIL" +echo "" +echo "Security Fixes Status:" +if [ $RACE_DETECTED -eq 1 ]; then + echo " 1. CWE-362 Race Condition: FAILED (still vulnerable)" +else + echo " 1. CWE-362 Race Condition: FIXED" +fi +if [ $NULL_CRASH_1 -eq 1 ] || [ $NULL_CRASH_2 -eq 1 ] || [ $NULL_CRASH_3 -eq 1 ] || [ $NULL_CRASH_4 -eq 1 ]; then + echo " 2. CWE-476 NULL Dereference: FAILED (still crashes)" +else + echo " 2. CWE-476 NULL Dereference: FIXED" +fi +if [ $BASE64URL_WORKS -eq 1 ]; then + echo " 3. Base64/Base64URL Support: WORKING" +else + echo " 3. Base64/Base64URL Support: FAILED" +fi +echo "" + +############################################# +# MEDIUM ISSUE 4: Integer Overflow atoi (CWE-190) +############################################# +echo "" +echo "########################################" +echo "# MEDIUM 4: Integer Overflow (CWE-190)" +echo "########################################" +echo "" +echo "atoi() has undefined behavior for out-of-range values." +echo "Fix: Use strtol() with proper validation and range check (1-1000)." +echo "" + +INT_VALIDATION_WORKS=1 + +# Test 4a: Very large max_len value - should be rejected +echo "--- Test 4a: Very large max_len (2147483648 = INT_MAX+1) ---" +echo "Input: max_len=2147483648" +echo "" +set +e +OUTPUT_4A=$(timeout 5 ./jwtcrack eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.cAOIAifu3fykvhkHpbuhbvtH807-Z2rI1FS3vX1XMjE abc 2147483648 sha256 2>&1) +EXIT_4A=$? +set -e +echo "$OUTPUT_4A" +echo "" +echo "Exit code: $EXIT_4A" +if echo "$OUTPUT_4A" | grep -q "Invalid max_len\|defaults to"; then + echo "PASS: Large value properly rejected with error message" +else + echo "FAIL: Large value not properly validated" + INT_VALIDATION_WORKS=0 +fi + +# Test 4b: Negative value - should be rejected +echo "" +echo "--- Test 4b: Negative max_len ---" +echo "Input: max_len=-1" +echo "" +set +e +OUTPUT_4B=$(./jwtcrack eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.cAOIAifu3fykvhkHpbuhbvtH807-Z2rI1FS3vX1XMjE abc -1 sha256 2>&1) +EXIT_4B=$? +set -e +echo "$OUTPUT_4B" +echo "" +echo "Exit code: $EXIT_4B" +if echo "$OUTPUT_4B" | grep -q "Invalid max_len\|defaults to"; then + echo "PASS: Negative value properly rejected" +else + echo "FAIL: Negative value not properly validated" + INT_VALIDATION_WORKS=0 +fi + +# Test 4c: Non-numeric value - should be rejected +echo "" +echo "--- Test 4c: Non-numeric max_len ---" +echo "Input: max_len=abc" +echo "" +set +e +OUTPUT_4C=$(./jwtcrack eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.cAOIAifu3fykvhkHpbuhbvtH807-Z2rI1FS3vX1XMjE abc abc sha256 2>&1) +EXIT_4C=$? +set -e +echo "$OUTPUT_4C" +echo "" +echo "Exit code: $EXIT_4C" +if echo "$OUTPUT_4C" | grep -q "Invalid max_len\|defaults to"; then + echo "PASS: Non-numeric value properly rejected" +else + echo "FAIL: Non-numeric value not properly validated" + INT_VALIDATION_WORKS=0 +fi + +# Test 4d: Value over 1000 - should be rejected (new upper bound) +echo "" +echo "--- Test 4d: max_len over 1000 ---" +echo "Input: max_len=1001" +echo "" +set +e +OUTPUT_4D=$(./jwtcrack eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.cAOIAifu3fykvhkHpbuhbvtH807-Z2rI1FS3vX1XMjE abc 1001 sha256 2>&1) +EXIT_4D=$? +set -e +echo "$OUTPUT_4D" +echo "" +echo "Exit code: $EXIT_4D" +if echo "$OUTPUT_4D" | grep -q "Invalid max_len\|defaults to"; then + echo "PASS: Value over 1000 properly rejected" +else + echo "FAIL: Value over 1000 not properly validated" + INT_VALIDATION_WORKS=0 +fi + +if [ $INT_VALIDATION_WORKS -eq 1 ]; then + echo "" + echo "==> CWE-190 INTEGER OVERFLOW: FIXED" + echo " strtol() with proper validation now used" + ((PASS++)) +else + echo "" + echo "==> CWE-190 INTEGER OVERFLOW: ISSUE PRESENT" + ((FAIL++)) +fi +((TOTAL++)) + +############################################# +# MEDIUM ISSUE 5: VLA Stack Overflow (CWE-121) +############################################# +echo "" +echo "########################################" +echo "# MEDIUM 5: VLA Stack Overflow (CWE-121)" +echo "########################################" +echo "" +echo "Variable-length array on stack can overflow" +echo "with large alphabet sizes." +echo "Fix: Heap allocate thread data array instead of VLA." +echo "" + +VLA_WORKS=1 + +# Test 5a: Very large alphabet (10000 characters) +echo "--- Test 5a: Large alphabet (10000 chars) ---" +echo "Previously would create VLA of 10000 pointers on stack" +echo "" + +# Generate a large alphabet +LARGE_ALPHABET=$(printf 'a%.0s' $(seq 1 10000)) +echo "Alphabet length: ${#LARGE_ALPHABET}" +echo "" + +set +e +timeout 5 ./jwtcrack eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.cAOIAifu3fykvhkHpbuhbvtH807-Z2rI1FS3vX1XMjE "$LARGE_ALPHABET" 1 sha256 2>&1 +EXIT_5A=$? +set -e +echo "" +echo "Exit code: $EXIT_5A" +if [ $EXIT_5A -eq 139 ] || [ $EXIT_5A -eq 134 ] || [ $EXIT_5A -eq 137 ] || [ $EXIT_5A -eq 136 ]; then + echo "FAIL: Stack overflow with large alphabet" + VLA_WORKS=0 +else + echo "PASS: Large alphabet handled without stack overflow" +fi + +# Test 5b: Even larger alphabet (100000 characters) +echo "" +echo "--- Test 5b: Very large alphabet (100000 chars) ---" +echo "" + +VERY_LARGE_ALPHABET=$(printf 'a%.0s' $(seq 1 100000)) +echo "Alphabet length: ${#VERY_LARGE_ALPHABET}" +echo "" + +set +e +timeout 5 ./jwtcrack eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.cAOIAifu3fykvhkHpbuhbvtH807-Z2rI1FS3vX1XMjE "$VERY_LARGE_ALPHABET" 1 sha256 2>&1 +EXIT_5B=$? +set -e +echo "" +echo "Exit code: $EXIT_5B" +if [ $EXIT_5B -eq 139 ] || [ $EXIT_5B -eq 134 ] || [ $EXIT_5B -eq 137 ] || [ $EXIT_5B -eq 136 ]; then + echo "FAIL: Stack overflow with very large alphabet" + VLA_WORKS=0 +else + echo "PASS: Very large alphabet handled without stack overflow" +fi + +# Test 5c: Verify heap allocation in code +echo "" +echo "--- Test 5c: Verify heap allocation in code ---" +if grep -q "struct s_thread_data \*\*pointers_data = malloc" /opt/src/main.c; then + echo "PASS: Thread data array is heap allocated" +else + echo "FAIL: Thread data array not heap allocated" + VLA_WORKS=0 +fi + +if [ $VLA_WORKS -eq 1 ]; then + echo "" + echo "==> CWE-121 VLA STACK OVERFLOW: FIXED" + echo " Thread data array now heap allocated" + ((PASS++)) +else + echo "" + echo "==> CWE-121 VLA STACK OVERFLOW: ISSUE PRESENT" + ((FAIL++)) +fi +((TOTAL++)) + +############################################# +# MEDIUM ISSUE 6: Unchecked malloc (CWE-252) +############################################# +echo "" +echo "########################################" +echo "# MEDIUM 6: Unchecked malloc (CWE-252)" +echo "########################################" +echo "" +echo "malloc() return values not checked for NULL." +echo "Fix: Add NULL checks after all malloc calls." +echo "" + +MALLOC_CHECKED=1 + +echo "--- Test 6a: Verify malloc NULL checks in code ---" +echo "" + +# Count malloc calls that have NULL checks nearby +TOTAL_MALLOCS=$(grep -c "malloc(" /opt/src/main.c || echo 0) +CHECKED_MALLOCS=$(grep -B1 -A1 "malloc(" /opt/src/main.c | grep -c "== NULL\|!= NULL" || echo 0) + +echo "Total malloc calls: $TOTAL_MALLOCS" +echo "Malloc calls with NULL checks: $CHECKED_MALLOCS" +echo "" + +# Check specific malloc patterns +echo "--- Test 6b: Verify specific malloc NULL checks ---" +echo "" + +# Check init_thread_data returns error on malloc failure +if grep -q "if (data->g_result == NULL)" /opt/src/main.c && grep -q "if (data->g_buffer == NULL)" /opt/src/main.c; then + echo "PASS: init_thread_data checks malloc returns" +else + echo "FAIL: init_thread_data missing malloc checks" + MALLOC_CHECKED=0 +fi + +# Check g_to_encrypt malloc +if grep -q "if (g_to_encrypt == NULL)" /opt/src/main.c; then + echo "PASS: g_to_encrypt malloc is checked" +else + echo "FAIL: g_to_encrypt malloc not checked" + MALLOC_CHECKED=0 +fi + +# Check g_signature malloc +if grep -q "if (g_signature == NULL)" /opt/src/main.c; then + echo "PASS: g_signature malloc is checked" +else + echo "FAIL: g_signature malloc not checked" + MALLOC_CHECKED=0 +fi + +# Check pointers_data malloc +if grep -q "if (pointers_data == NULL)" /opt/src/main.c; then + echo "PASS: pointers_data malloc is checked" +else + echo "FAIL: pointers_data malloc not checked" + MALLOC_CHECKED=0 +fi + +# Check tid malloc +if grep -q "if (tid == NULL)" /opt/src/main.c; then + echo "PASS: tid malloc is checked" +else + echo "FAIL: tid malloc not checked" + MALLOC_CHECKED=0 +fi + +echo "" +if [ $MALLOC_CHECKED -eq 1 ]; then + echo "==> CWE-252 UNCHECKED MALLOC: FIXED" + echo " All malloc returns are now validated" + ((PASS++)) +else + echo "==> CWE-252 UNCHECKED MALLOC: ISSUE PRESENT" + ((FAIL++)) +fi +((TOTAL++)) + +############################################# +# MEDIUM ISSUE 7: Timing Side-Channel (CWE-208) +############################################# +echo "" +echo "########################################" +echo "# HIGH 7: Timing Side-Channel (CWE-208)" +echo "########################################" +echo "" +echo "memcmp() is not constant-time and may leak" +echo "information via timing differences." +echo "Fix: Use constant-time comparison function." +echo "" + +TIMING_FIXED=1 + +echo "--- Test 7a: Verify constant-time compare function exists ---" +if grep -q "constant_time_compare" /opt/src/main.c; then + echo "PASS: constant_time_compare function found" +else + echo "FAIL: constant_time_compare function not found" + TIMING_FIXED=0 +fi + +echo "" +echo "--- Test 7b: Verify memcmp is NOT used for signature comparison ---" +if grep -q "memcmp.*g_signature\|memcmp.*g_result" /opt/src/main.c; then + echo "FAIL: memcmp still used for signature comparison" + TIMING_FIXED=0 +else + echo "PASS: memcmp not used for signature comparison" +fi + +echo "" +echo "--- Test 7c: Verify constant_time_compare is used ---" +if grep -q "constant_time_compare(data->g_result, g_signature" /opt/src/main.c; then + echo "PASS: constant_time_compare used for signature comparison" +else + echo "FAIL: constant_time_compare not used for signature comparison" + TIMING_FIXED=0 +fi + +echo "" +if [ $TIMING_FIXED -eq 1 ]; then + echo "==> CWE-208 TIMING ATTACK: FIXED" + echo " constant_time_compare() now used for signature comparison" + ((PASS++)) +else + echo "==> CWE-208 TIMING ATTACK: ISSUE PRESENT" + ((FAIL++)) +fi +((TOTAL++)) + +############################################# +# SUMMARY +############################################# +echo "" +echo "========================================" +echo "SECURITY TEST SUMMARY" +echo "========================================" +echo "" +echo "Total test categories: $TOTAL" +echo "Fixes verified: $PASS" +echo "Issues remaining: $FAIL" +echo "" +echo "Critical Security Fixes:" +if [ $RACE_DETECTED -eq 1 ]; then + echo " 1. CWE-362 Race Condition: FAILED (still vulnerable)" +else + echo " 1. CWE-362 Race Condition: FIXED" +fi +if [ $NULL_CRASH_1 -eq 1 ] || [ $NULL_CRASH_2 -eq 1 ] || [ $NULL_CRASH_3 -eq 1 ] || [ $NULL_CRASH_4 -eq 1 ]; then + echo " 2. CWE-476 NULL Dereference: FAILED (still crashes)" +else + echo " 2. CWE-476 NULL Dereference: FIXED" +fi +if [ $BASE64URL_WORKS -eq 1 ]; then + echo " 3. Base64/Base64URL Support: WORKING" +else + echo " 3. Base64/Base64URL Support: FAILED" +fi +echo "" +echo "Medium/High Priority Fixes:" +if [ $INT_VALIDATION_WORKS -eq 1 ]; then + echo " 4. CWE-190 Integer Overflow: FIXED" +else + echo " 4. CWE-190 Integer Overflow: FAILED" +fi +if [ $VLA_WORKS -eq 1 ]; then + echo " 5. CWE-121 VLA Stack Overflow: FIXED" +else + echo " 5. CWE-121 VLA Stack Overflow: FAILED" +fi +if [ $MALLOC_CHECKED -eq 1 ]; then + echo " 6. CWE-252 Unchecked malloc: FIXED" +else + echo " 6. CWE-252 Unchecked malloc: FAILED" +fi +if [ $TIMING_FIXED -eq 1 ]; then + echo " 7. CWE-208 Timing Attack: FIXED" +else + echo " 7. CWE-208 Timing Attack: FAILED" +fi +echo "" + +# Also run standard functional tests +echo "========================================" +echo "STANDARD FUNCTIONAL TESTS" +echo "========================================" +echo "" + +echo "--- Functional Test: HS256 (secret: Sn1f) ---" +RESULT=$(./jwtcrack eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.cAOIAifu3fykvhkHpbuhbvtH807-Z2rI1FS3vX1XMjE ABCSNFabcsnf1234567 5 sha256) +echo "$RESULT" +if echo "$RESULT" | grep -q 'Secret is "Sn1f"'; then + echo "PASSED" +else + echo "FAILED" +fi + +echo "" +echo "--- Memory Test: Valgrind leak check ---" +set +e +valgrind --leak-check=full --error-exitcode=1 \ + ./jwtcrack eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.cAOIAifu3fykvhkHpbuhbvtH807-Z2rI1FS3vX1XMjE ABCSNFabcsnf1234567 5 sha256 +VALGRIND_EXIT=$? +set -e +if [ $VALGRIND_EXIT -eq 0 ]; then + echo "PASSED - No memory leaks" +else + echo "FAILED - Memory leaks detected" +fi + +echo "" +echo "========================================" +echo "TEST SUITE COMPLETE" +echo "========================================" From 9424892f8c5879dbc9a36588df700f035aecf32d Mon Sep 17 00:00:00 2001 From: Maxim Masiutin Date: Mon, 1 Dec 2025 21:02:52 +0200 Subject: [PATCH 2/2] Made the action periodic --- .github/workflows/trivy-analysis.yaml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/trivy-analysis.yaml b/.github/workflows/trivy-analysis.yaml index a260b9f..30e60bd 100644 --- a/.github/workflows/trivy-analysis.yaml +++ b/.github/workflows/trivy-analysis.yaml @@ -9,6 +9,8 @@ on: pull_request: workflow_dispatch: push: + schedule: + - cron: '0 0 1 */3 *' env: SARIF_FILE: 'trivy-results.sarif' @@ -24,7 +26,7 @@ jobs: - name: Run Trivy vulnerability scanner on the cloned repository files uses: aquasecurity/trivy-action@0.33.1 with: - version: 'v0.67.0' + version: 'v0.67.2' scan-type: 'fs' scanners: 'vuln,misconfig,secret,license' ignore-unfixed: true @@ -41,7 +43,7 @@ jobs: ls -lash ${{ env.SARIF_FILE }} - name: Upload Trivy scan results to GitHub Security tab - uses: github/codeql-action/upload-sarif@v4 + uses: github/codeql-action/upload-sarif@v4.31.6 with: sarif_file: ${{ env.SARIF_FILE }}