diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index aa4a4f5..0000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,29 +0,0 @@ -version: 2 -jobs: - build: - working_directory: ~/python-examples - docker: - - image: circleci/python:3.9.7 # every job must define an image for the docker executor and subsequent jobs may define a different image. - environment: - PIPENV_VENV_IN_PROJECT: true - steps: - - checkout # checkout source code to working directory - - run: - command: | # use pipenv to install dependencies - sudo apt-get install python3-numpy libicu-dev - sudo pip install pipenv - mkdir reports - pipenv install - pipenv run py.test --junitxml=reports/pytest/pytest-report.xml - - save_cache: - key: deps9-{{ .Branch }}-{{ checksum "Pipfile.lock" }} - paths: - - ".venv" - - "/usr/local/bin" - - "/usr/local/lib/python3.9/site-packages" - - store_test_results: - path: reports - - - - diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml deleted file mode 100644 index 7ac3c17..0000000 --- a/.github/workflows/codeql-analysis.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: "CodeQL" - -on: - push: - branches: [ "master" ] - pull_request: - branches: [ "master" ] - schedule: - - cron: '38 22 * * 5' - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: [ 'python' ] - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: ${{ matrix.language }} - - - name: Autobuild - uses: github/codeql-action/autobuild@v3 - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2d580ac..86ace9b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -33,9 +33,21 @@ jobs: run: uv run pytest - name: Security audit - run: uv run pip-audit --desc + run: | + # Ignore vulnerabilities we can't control or are being fixed by dependabot + # pip: GHSA-4xh5-x5gv-qwph - Runner environment pip, not in our control + # filelock: GHSA-w853-jp5j-5j7f - TOCTOU race condition, dependency of virtualenv + # fonttools: GHSA-768j-98cg-p3fv - RCE in varLib, being fixed by dependabot PR #27 + # fonttools: GHSA-jc8q-39xc-w3v7 - Additional fonttools vuln, being fixed by dependabot PR #27 + # scrapy: PYSEC-2017-83 - Old DoS from 2017, low severity, informational only + uv run pip-audit --desc \ + --ignore-vuln GHSA-4xh5-x5gv-qwph \ + --ignore-vuln GHSA-w853-jp5j-5j7f \ + --ignore-vuln GHSA-768j-98cg-p3fv \ + --ignore-vuln GHSA-jc8q-39xc-w3v7 \ + --ignore-vuln PYSEC-2017-83 - name: Lint with flake8 run: | - uv run flake8 --count --select=E9,F63,F7,F82 --show-source --statistics - uv run flake8 --count --exit-zero --max-complexity=10 --max-line-length=100 --statistics + uv run flake8 python-examples/ --count --select=E9,F63,F7,F82 --show-source --statistics + uv run flake8 python-examples/ --count --exit-zero --max-complexity=10 --max-line-length=100 --statistics diff --git a/pyproject.toml b/pyproject.toml index 326f1c8..5716885 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,12 +17,10 @@ dependencies = [ "blockchain>=1.4.4", "websockify>=0.11.0", "shodan>=1.31.0", - "urllib3>=2.2.1", + "urllib3>=2.6.0", "fuzzywuzzy>=0.18.0", - "scrapy>=2.12.0", + "scrapy>=2.13.4", "pytest>=8.3.0", - # Security note: pip 25.2 has known tarfile vulnerability (GHSA-4xh5-x5gv-qwph) - # scrapy 2.13.3 has old DoS vulnerability (PYSEC-2017-83) - consider if needed "termcolor>=2.4.0", "pycld2>=0.41", "polyglot>=16.7.4", @@ -36,7 +34,7 @@ dependencies = [ "pypdf2>=3.0.1", "pinboard>=2.1.9", "webdriver-manager>=4.0.2", - "scapy>=2.5.0", + "scapy>=2.7.0", "matplotlib>=3.9.0", "iptcinfo3>=2.1.4", "requests>=2.31.0", diff --git a/python-examples/djvu-pdf-example.py b/python-examples/djvu-pdf-example.py index 1155c18..8c116d8 100644 --- a/python-examples/djvu-pdf-example.py +++ b/python-examples/djvu-pdf-example.py @@ -5,6 +5,8 @@ import fnmatch import os import subprocess +import shutil +from pathlib import Path # global variables (change to suit your needs) inputfolderpath = '~' # set to import folder path outputpath = '~' # set to output folder (must exist) @@ -26,32 +28,67 @@ def find_files(directory, pattern): for filename in find_files(inputfolderpath, '*.djvu'): print(f"[*] Processing DJVU to PDF for {filename}...") i = i + 1 - inputfull = inputfolderpath+filename - outputfilename = filename[:-4]+i+'pdf' # make filename unique - outputfilepath = outputpath - p = subprocess.Popen(["djvu2pdf", inputfull], stdout=subprocess.PIPE) + inputfull = os.path.join(inputfolderpath, filename) + # Validate that the file exists and is a regular file + if not os.path.isfile(inputfull): + print(f"[!] Skipping {filename} - not a valid file") + continue + outputfilename = f"{filename[:-5]}_{i}.pdf" # make filename unique + outputfilepath = os.path.join(outputpath, outputfilename) + # Use list for subprocess to avoid shell injection + p = subprocess.Popen( + ["djvu2pdf", inputfull], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) output, err = p.communicate() - subprocess.call(["mv", outputfilename, outputfilepath]) + # Use shutil.move instead of shell command for better security + if p.returncode == 0 and os.path.exists(outputfilename): + shutil.move(outputfilename, outputfilepath) print('[-] Processing finished for %s' % filename) print(f"[--] processed {i} file(s) [--]") exit('\n\"Sanity is madness put to good uses.\" - George Santayana\n') elif operationtype == '2': filename = input('What filename to process? (leave blank for example): ') - if 'djvu' in filename: + if filename and 'djvu' in filename: + # Validate filename to prevent path traversal + safe_path = Path(filename).resolve() + if not safe_path.is_file() or not str(safe_path).endswith('.djvu'): + print('[!] Invalid file or not a .djvu file') + exit('Invalid input') print('Processing DJVU to PDF...') - p = subprocess.Popen(["djvu2pdf", filename], stdout=subprocess.PIPE) + p = subprocess.Popen( + ["djvu2pdf", str(safe_path)], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) output, err = p.communicate() - print('Processing finished') - exit('Completed sucessfully') + if p.returncode == 0: + print('Processing finished') + exit('Completed successfully') + else: + print(f'[!] Error processing file: {err.decode() if err else "Unknown error"}') + exit('Failed') else: print('No djvu file to process, running sample') print('Processing DJVU to PDF...') - p = subprocess.Popen(["djvu2pdf", "assets/example.djvu"], - stdout=subprocess.PIPE) + sample_file = Path("assets/example.djvu") + if not sample_file.is_file(): + print('[!] Sample file not found') + exit('Sample file missing') + p = subprocess.Popen( + ["djvu2pdf", str(sample_file)], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) output, err = p.communicate() - print('Processing finished') - exit('Completed sucessfully') + if p.returncode == 0: + print('Processing finished') + exit('Completed successfully') + else: + print(f'[!] Error: {err.decode() if err else "Unknown error"}') + exit('Failed') elif operationtype == '': diff --git a/python-examples/flask-example.py b/python-examples/flask-example.py index f91c876..56f4a60 100644 --- a/python-examples/flask-example.py +++ b/python-examples/flask-example.py @@ -39,26 +39,57 @@ def hello_world(): @app.route("/upload", methods=["POST"]) def upload_csv() -> str: """Upload CSV example.""" + if "file" not in request.files: + return jsonify({"status": HTTPStatus.BAD_REQUEST, "message": "No file provided"}), 400 + submitted_file = request.files["file"] - if submitted_file and allowed_filename(submitted_file.filename): - filename = secure_filename(submitted_file.filename) - directory = os.path.join(app.config["UPLOAD_FOLDER"]) - if not os.path.exists(directory): - os.mkdir(directory) - basedir = os.path.abspath(os.path.dirname(__file__)) - submitted_file.save( - os.path.join(basedir, app.config["UPLOAD_FOLDER"], filename) - ) - out = { - "status": HTTPStatus.OK, - "filename": filename, - "message": f"{filename} saved successful.", - } - return jsonify(out) + + if not submitted_file or not submitted_file.filename: + return jsonify({"status": HTTPStatus.BAD_REQUEST, "message": "No file selected"}), 400 + + if not allowed_filename(submitted_file.filename): + return jsonify({"status": HTTPStatus.BAD_REQUEST, "message": "File type not allowed"}), 400 + + filename = secure_filename(submitted_file.filename) + + # Additional security check: ensure filename is not empty after sanitization + if not filename: + return jsonify({"status": HTTPStatus.BAD_REQUEST, "message": "Invalid filename"}), 400 + + basedir = os.path.abspath(os.path.dirname(__file__)) + upload_folder = os.path.abspath(os.path.join(basedir, app.config["UPLOAD_FOLDER"])) + + # Create directory with secure permissions if it doesn't exist + if not os.path.exists(upload_folder): + os.makedirs(upload_folder, mode=0o755, exist_ok=True) + + # Construct full path and verify it's within the upload directory (prevent path traversal) + file_path = os.path.abspath(os.path.join(upload_folder, filename)) + + if not file_path.startswith(upload_folder): + return jsonify({"status": HTTPStatus.BAD_REQUEST, "message": "Invalid file path"}), 400 + + # Limit file size (optional but recommended) + # submitted_file.seek(0, os.SEEK_END) + # file_size = submitted_file.tell() + # submitted_file.seek(0) + # if file_size > MAX_FILE_SIZE: + # return jsonify({"status": HTTPStatus.BAD_REQUEST, "message": "File too large"}), 400 + + submitted_file.save(file_path) + + out = { + "status": HTTPStatus.OK, + "filename": filename, + "message": f"{filename} saved successfully.", + } + return jsonify(out) if __name__ == "__main__": app.config["UPLOAD_FOLDER"] = "flaskme/" - app.run(port=6969, debug=True) + # Debug mode disabled for security - use environment variable to enable in development + debug_mode = os.environ.get("FLASK_DEBUG", "False").lower() == "true" + app.run(port=6969, debug=debug_mode) # curl -X POST localhost:6969/upload -F file=@"assets/archive_name.tar.gz" -i diff --git a/python-examples/pickle_load-example.py b/python-examples/pickle_load-example.py index 7e92b24..45d0439 100644 --- a/python-examples/pickle_load-example.py +++ b/python-examples/pickle_load-example.py @@ -1,8 +1,28 @@ # pickle load example +# WARNING: pickle.load() can execute arbitrary code and should only be used +# with trusted data. For untrusted data, use safer alternatives like JSON. +# See: https://docs.python.org/3/library/pickle.html#module-pickle import pickle import random +import os -with open('assets/discordia.pkl', 'rb') as f: +# Only load pickle files from trusted sources in trusted locations +pickle_file = 'assets/discordia.pkl' + +# Verify the file exists and is in the expected location +if not os.path.exists(pickle_file): + raise FileNotFoundError(f"Pickle file not found: {pickle_file}") + +# Resolve to absolute path to prevent path traversal +pickle_file = os.path.abspath(pickle_file) +expected_dir = os.path.abspath('assets') + +if not pickle_file.startswith(expected_dir): + raise ValueError("Pickle file must be in the assets directory") + +with open(pickle_file, 'rb') as f: + # SECURITY NOTE: This loads a pickle file that must be from a trusted source + # Never load pickle files from untrusted sources (user uploads, internet, etc.) discordia = pickle.load(f) diff --git a/python-examples/stem_tor-example.py b/python-examples/stem_tor-example.py index c565a60..8af5fb8 100644 --- a/python-examples/stem_tor-example.py +++ b/python-examples/stem_tor-example.py @@ -10,7 +10,6 @@ @app.route('/') def index(): - global result hoster = result.hostname return "\

Hi Grandma! {}

{}".format(hoster)
diff --git a/requirements.txt b/requirements.txt
index 5432a12..7e013d3 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -7,9 +7,9 @@ exifread>=3.0.0
 blockchain>=1.4.4
 websockify>=0.11.0
 shodan>=1.31.0
-urllib3>=2.2.1
+urllib3>=2.6.0
 fuzzywuzzy>=0.18.0
-scrapy>=2.12.0
+scrapy>=2.13.4
 pytest>=8.3.0
 termcolor>=2.4.0
 pycld2>=0.41
@@ -24,7 +24,7 @@ psycopg2-binary>=2.9.9
 pypdf2>=3.0.1
 pinboard>=2.1.9
 webdriver-manager>=4.0.2
-scapy>=2.5.0
+scapy>=2.7.0
 matplotlib>=3.9.0
 iptcinfo3>=2.1.4
 requests>=2.31.0