diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..77ba709 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,33 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/ubuntu +{ + "name": "Ubuntu", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "mcr.microsoft.com/devcontainers/base:jammy", + "features": { + "ghcr.io/devcontainers/features/github-cli:1": {}, + "ghcr.io/devcontainers/features/powershell:1": {} + }, + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "uname -a", + // Configure tool-specific properties. + "customizations": { + "vscode": { + "extensions": [ + "humao.rest-client", + "github.copilot", + "github.copilot-chat", + "github.copilot-labs", + "ms-sarifvscode.sarif-viewer", + "Vue.volar", + "vsls-contrib.codetour" + ] + } + } + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/.github/codeql/codeql-config.yml b/.github/codeql/codeql-config.yml new file mode 100644 index 0000000..be7fa80 --- /dev/null +++ b/.github/codeql/codeql-config.yml @@ -0,0 +1,8 @@ +name: "CodeQL config" +queries: + - name: Run custom queries + uses: ./queries/vue-xss.ql + +query-filters: + - exclude: + id: js/xss diff --git a/.github/secret_scanning.yml b/.github/secret_scanning.yml new file mode 100644 index 0000000..9e65813 --- /dev/null +++ b/.github/secret_scanning.yml @@ -0,0 +1,3 @@ +paths-ignore: + - 'README.md' + - 'exercises/*' diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..9406eb8 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,84 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ "main" ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ "main" ] + schedule: + - cron: '33 7 * * 6' + workflow_dispatch: + +jobs: + analyze: + name: Analyze + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners + # Consider using larger runners for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'go', 'java', 'javascript', 'python' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby', 'swift' ] + # Use only 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use only 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + config-file: .github/codeql/codeql-config.yml + + + # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: "/language:${{matrix.language}}" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bc47f47 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode +# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode diff --git a/.tours/lab-1-exercise-1.tour b/.tours/lab-1-exercise-1.tour new file mode 100644 index 0000000..37d28a1 --- /dev/null +++ b/.tours/lab-1-exercise-1.tour @@ -0,0 +1,21 @@ +{ + "$schema": "https://aka.ms/codetour-schema", + "title": "Lab-1-Exercise-1", + "steps": [ + { + "file": "storage/src/main/resources/application.properties", + "description": "This is the secret pattern `mona_value_abc124` for which we will create an AI generated custom pattern for Secret Scanning in GitHub Advanced Security", + "line": 5, + "selection": { + "start": { + "line": 5, + "character": 16 + }, + "end": { + "line": 5, + "character": 33 + } + } + } + ] +} diff --git a/.tours/lab-2-exercise-1.tour b/.tours/lab-2-exercise-1.tour new file mode 100644 index 0000000..05e323b --- /dev/null +++ b/.tours/lab-2-exercise-1.tour @@ -0,0 +1,26 @@ +{ + "$schema": "https://aka.ms/codetour-schema", + "title": "Lab-2-Exercise-1", + "steps": [ + { + "file": "frontend/src/components/Gallery.vue", + "description": "Add the Sanitization function listed in the exercise description here", + "line": 233, + "selection": { + "start": { + "line": 233, + "character": 1 + }, + "end": { + "line": 233, + "character": 33 + } + } + }, + { + "file": "frontend/src/components/Gallery.vue", + "description": "Call the sanization function from the Update function by placing the following call\n\n artItem.description = sanitizeInput(artItem.description)\n artItem.title = sanitizeInput(artItem.title)", + "line": 339 + } + ] +} diff --git a/.tours/lab-2-exercise-2.tour b/.tours/lab-2-exercise-2.tour new file mode 100644 index 0000000..b505674 --- /dev/null +++ b/.tours/lab-2-exercise-2.tour @@ -0,0 +1,21 @@ +{ + "$schema": "https://aka.ms/codetour-schema", + "title": "Lab-2-Exercise-2", + "steps": [ + { + "file": "gallery/main.go", + "description": "Select this line of code which has an Injection vulnerability and we will ask GitHub Copilot to explain what an Injection Vulnerability is and how it can be exploited. Below prompt can be used:\n\n\"Explain me what is an SQL Injection and how it can be exploited with my code as a reference. Also explain me how the vulnerability can be fixed\"", + "line": 198, + "selection": { + "start": { + "line": 198, + "character": 2 + }, + "end": { + "line": 198, + "character": 153 + } + } + } + ] +} diff --git a/.tours/lab-2-exercise-3.tour b/.tours/lab-2-exercise-3.tour new file mode 100644 index 0000000..2e16513 --- /dev/null +++ b/.tours/lab-2-exercise-3.tour @@ -0,0 +1,21 @@ +{ + "$schema": "https://aka.ms/codetour-schema", + "title": "Lab-2-Exercise-3", + "steps": [ + { + "file": "gallery/main.go", + "description": "Ask GitHub Copilot for a fix for this SQL Injection vulnerability.", + "line": 308, + "selection": { + "start": { + "line": 308, + "character": 2 + }, + "end": { + "line": 308, + "character": 199 + } + } + } + ] +} diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..71d0088 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 github + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..14c5167 --- /dev/null +++ b/README.md @@ -0,0 +1,63 @@ +

Harnessing AI: Next Level Strategies for Advanced Security

+
@s-samadi
+
@abhi-dutta
+ +

+ Introduction • + Prerequisites • + Mona Gallery • + Learning Objectives • + Learning Resources +

+ + + +### Introduction + +This repository contains the source code for the `Mona Gallery` vulnerable web application. The exercises for this workshop can be found in the `exercises.md` file. + +### Prerequisites + +Please make sure that you have all the prerequisites in place before we start of the wokshop + +1) Create a codespace from the repository +
+ Demo + + ![create-codespace](https://github.com/octodemo/universe-wip/assets/68650974/6dde8598-0cd3-4b62-ae60-c609ea4e27c7) + +
+ + +2) Verify that the GitHub Copilot, GitHub Copilot Chat, and GitHub Copilot Lab plugins are pre-installed in your codespace. These installations should occur automatically when you start the codespace. +3) Configure the plugin to grant you access to GitHub copilot through the [githubuniverseworkshops](https://github.com/githubuniverseworkshops) Org that you have been granted access to +4) Confirm that Advanced Security and all its features have been enabled on your repository + +### Mona Gallery + +The Mona Gallery is a delibrately vulnerable web application consisting of several prevalent vulnerability types, such as SQL injection, XSS, and deserialization, among others. The application's codebase is diverse, utilizing multiple technologies, including Go, Python, Javascript, and Java. A architecture diagram can be found below. We will use this application's codebase for this workshop. + + +![mona-gallery](https://github.com/octodemo/universe-wip/assets/68650974/cb0bbf88-6d68-49e8-9129-fa3e487b2be9) + +#### Architecture Diagram + +The application's frontend is built with Vue.js 3 and Bootstrap 5, while authorization is managed through the Zitadel OIDC service implemented in Go. Middleware functions are handled in Python. The API is developed in Go, and Blob storage is implemented with MinIO, written in Java. Furthermore, the API layer is also implemented in Go, and the database relies on SQL Lite. Each of these services is encapsulated in its respective Docker container, resulting in a total of five images. To run the application, you can utilize Docker Compose. + + +![image](https://github.com/octodemo/universe-wip/assets/79184790/34600cdc-5dde-4dc4-9a68-8e31709c1ec0) + +### Learning Objectives + + - Hands on exercise demonstrating our new feature to generate regexes using AI + - Use AI to find generic secrets + - Practical lab demonstrating the new autofix feature for Javascript CodeQL alerts on the pull request + - How to use GitHub Copilot to learn about CodeQL + - Use GitHub Copilot to learn about application security + +### Learning Resources + + - [GitHub Advanced Security Learning Path - Microsoft Learn](https://learn.microsoft.com/en-us/collections/rqymc6yw8q5rey) + - [Docs - GitHub Advanced Security](https://docs.github.com/en/enterprise-cloud@latest/get-started/learning-about-github/about-github-advanced-security) + - [GitHub Copilot Learning Path - Microsoft Learn](https://learn.microsoft.com/en-us/training/modules/introduction-to-github-copilot/) + - [Docs - GitHub Copilot](https://docs.github.com/en/copilot) diff --git a/auth-ext/.gitignore b/auth-ext/.gitignore new file mode 100644 index 0000000..8dd0df6 --- /dev/null +++ b/auth-ext/.gitignore @@ -0,0 +1,188 @@ +# Created by https://www.toptal.com/developers/gitignore/api/python,virtualenv +# Edit at https://www.toptal.com/developers/gitignore?templates=python,virtualenv + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + +### VirtualEnv ### +# Virtualenv +# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ +[Bb]in +[Ii]nclude +[Ll]ib +[Ll]ib64 +[Ll]ocal +[Ss]cripts +pyvenv.cfg +pip-selfcheck.json + +# End of https://www.toptal.com/developers/gitignore/api/python,virtualenv diff --git a/auth-ext/app.py b/auth-ext/app.py new file mode 100644 index 0000000..b4cc379 --- /dev/null +++ b/auth-ext/app.py @@ -0,0 +1,89 @@ +import base64 +import os + +from flask import Flask, g, redirect, request, abort, session, url_for +from flask_cors import CORS + +import requests +import jwt +from datetime import datetime, timedelta, timezone + + +from dotenv import load_dotenv +load_dotenv() + +app = Flask(__name__) +CORS(app) + +CLIENT_ID = os.getenv("OIDC_CLIENT_ID") +CLIENT_SECRET = os.getenv("OIDC_CLIENT_SECRET") +OIDC_ISSUER = os.getenv("OIDC_ISSUER") + +def get_access_token(code): + app.logger.debug('Requesting access token') + credentials = base64.standard_b64encode( + f'{CLIENT_ID}:{CLIENT_SECRET}'.encode('ascii') + ).decode('ascii') + r = requests.post(f'{OIDC_ISSUER}oauth/token', data={ + 'code': code, + 'grant_type': 'authorization_code', + 'redirect_uri': 'http://localhost:5173/login/callback', #change back to 8080 + }, headers={ + 'Authorization': f'Basic {credentials}', + 'Accept': 'application/json', + 'Content-Type': 'application/x-www-form-urlencoded', + }) + + app.logger.debug('Received response %s with status %d', r.json(), r.status_code) + data = r.json() + + if 'error' in data: + return (None, data['error_description']) + + # return ({'access_token': data['access_token'], 'scope': data['scope'], 'token_type': data['token_type']}, None) + return (data, None) + +def get_user_profile(access_token): + app.logger.debug('Request user profile') + r = requests.get(f'{OIDC_ISSUER}userinfo', headers={ + 'Authorization': f'Bearer {access_token}' + }) + + app.logger.debug('Received response %s with status %d', r.json(), r.status_code) + data = r.json() + + if 'error' in data: + return (None, data['error_description']) + + return (data, None) + +@app.route("/authenticate/") +def authenticate(code): + app.logger.debug("Received authentication request with code: %s", code) + access_info, error = get_access_token(code) + + if error: + return {'error': error} + + profile, error = get_user_profile(access_info['access_token']) + if error: + return {'error': error} + + claimset = { + 'iss': "OctoGallery", + 'iat': int(datetime.now(timezone.utc).timestamp()), + 'exp': int((datetime.now(timezone.utc) + timedelta(days=1)).timestamp()), + 'profile': {k:v for k, v in profile.items() if k in ['login', 'name', 'email']} + } + app.logger.debug("Created claimset: %s", claimset) + + token = jwt.encode(claimset, 'secretsecret1234secretsecret1234', algorithm='HS256') + + return {'token': token.decode()} + +if __name__ == "__main__": + app.run( + host=os.environ.get("BACKEND_HOST", "127.0.0.1"), + port=5000, + debug=True + ) \ No newline at end of file diff --git a/auth-ext/requirements.txt b/auth-ext/requirements.txt new file mode 100644 index 0000000..9042a16 --- /dev/null +++ b/auth-ext/requirements.txt @@ -0,0 +1,25 @@ +asn1crypto==0.24.0 +certifi==2020.6.20 +chardet==3.0.4 +click==7.1.2 +cryptography==2.6.1 +entrypoints==0.3 +Flask==1.1.2 +Flask-Cors==3.0.8 +idna==2.10 +django-two-factor-auth==1.12 +itsdangerous==1.1.0 +Jinja2==2.11.2 +keyring==17.1. +keyrings.alt==3.1.1 +MarkupSafe==1.1.1 +pycrypto==2.6.1 +#PyGObject==3.30.4 +PyJWT==1.7.1 +python-dotenv==0.15.0 +pyxdg==0.25 +requests==2.24.0 +SecretStorage==2.3.1 +six==1.12.0 +urllib3==1.25.11 +Werkzeug==1.0.1 diff --git a/auth/.gitignore b/auth/.gitignore new file mode 100644 index 0000000..d19c362 --- /dev/null +++ b/auth/.gitignore @@ -0,0 +1,27 @@ +# Created by https://www.toptal.com/developers/gitignore/api/go +# Edit at https://www.toptal.com/developers/gitignore?templates=go + +### Go ### +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work + +# End of https://www.toptal.com/developers/gitignore/api/go diff --git a/auth/go.mod b/auth/go.mod new file mode 100644 index 0000000..01f7ead --- /dev/null +++ b/auth/go.mod @@ -0,0 +1,41 @@ +module auth + +go 1.19 + +require ( + github.com/golang/mock v1.6.0 + github.com/google/go-github/v31 v31.0.0 + github.com/google/uuid v1.3.1 + github.com/gorilla/mux v1.8.0 + github.com/gorilla/schema v1.2.0 + github.com/gorilla/securecookie v1.1.1 + github.com/jeremija/gosubmit v0.2.7 + github.com/muhlemmer/gu v0.3.1 + github.com/rs/cors v1.9.0 + github.com/sirupsen/logrus v1.9.3 + github.com/stretchr/testify v1.8.4 + github.com/zitadel/oidc v1.13.4 + github.com/zitadel/oidc/v2 v2.10.0 + go.opentelemetry.io/otel v1.17.0 + go.opentelemetry.io/otel/trace v1.17.0 + golang.org/x/oauth2 v0.11.0 + golang.org/x/text v0.12.0 + gopkg.in/square/go-jose.v2 v2.6.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-logr/logr v1.2.4 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + go.opentelemetry.io/otel/metric v1.17.0 // indirect + golang.org/x/crypto v0.12.0 // indirect + golang.org/x/net v0.14.0 // indirect + golang.org/x/sys v0.11.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/protobuf v1.31.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/auth/go.sum b/auth/go.sum new file mode 100644 index 0000000..3d7a3a8 --- /dev/null +++ b/auth/go.sum @@ -0,0 +1,104 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-github/v31 v31.0.0/go.mod h1:NQPZol8/1sMoWYGN2yaALIBytu17gAWfhbweiEed3pM= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= +github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc= +github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= +github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/jeremija/gosubmit v0.2.7/go.mod h1:Ui+HS073lCFREXBbdfrJzMB57OI/bdxTiLtrDHHhFPI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM= +github.com/muhlemmer/gu v0.3.1/go.mod h1:YHtHR+gxM+bKEIIs7Hmi9sPT3ZDUvTN/i88wQpZkrdM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rs/cors v1.9.0 h1:l9HGsTsHJcvW14Nk7J9KFz8bzeAWXn3CG6bgt7LsrAE= +github.com/rs/cors v1.9.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/zitadel/oidc v1.13.4 h1:+k2GKqP9Ld9S2MSFlj+KaNsoZ3J9oy+Ezw51EzSFuC8= +github.com/zitadel/oidc v1.13.4/go.mod h1:3h2DhUcP02YV6q/CA/BG4yla0o6rXjK+DkJGK/dwJfw= +github.com/zitadel/oidc/v2 v2.10.0 h1:mKCOA1SF7R+XmKmicbOAMY2wxp3szZMuM3IzrkBGplQ= +github.com/zitadel/oidc/v2 v2.10.0/go.mod h1:rEM7F10FKuieuQUlQf9fRoSiTkSs8rx5Dj4SgfsMPWU= +go.opentelemetry.io/otel v1.17.0 h1:MW+phZ6WZ5/uk2nd93ANk/6yJ+dVrvNWUjGhnnFU5jM= +go.opentelemetry.io/otel v1.17.0/go.mod h1:I2vmBGtFaODIVMBSTPVDlJSzBDNf93k60E6Ft0nyjo0= +go.opentelemetry.io/otel/metric v1.17.0 h1:iG6LGVz5Gh+IuO0jmgvpTB6YVrCGngi8QGm+pMd8Pdc= +go.opentelemetry.io/otel/metric v1.17.0/go.mod h1:h4skoxdZI17AxwITdmdZjjYJQH5nzijUUjm+wtPph5o= +go.opentelemetry.io/otel/trace v1.17.0 h1:/SWhSRHmDPOImIAetP1QAeMnZYiQXrTy4fMMYOdSKWQ= +go.opentelemetry.io/otel/trace v1.17.0/go.mod h1:I/4vKTgFclIsXRVucpH25X0mpFSczM7aHeaz0ZBLWjY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.11.0 h1:vPL4xzxBM4niKCW6g9whtaWVXTJf1U5e4aZxxFx/gbU= +golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= +gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/auth/main.go b/auth/main.go new file mode 100644 index 0000000..cc75577 --- /dev/null +++ b/auth/main.go @@ -0,0 +1,35 @@ +package main + +import ( + "fmt" + "log" + "net/http" + + "auth/op" + "auth/storage" +) + +func main() { + //we will run on :9998 + port := "9998" + //which gives us the issuer: http://localhost:9998/ + issuer := fmt.Sprintf("http://localhost:%s/", port) + + // the OpenIDProvider interface needs a Storage interface handling various checks and state manipulations + // this might be the layer for accessing your database + // in this example it will be handled in-memory + storage := storage.NewStorage(storage.NewUserStore(issuer)) + + router := op.SetupServer(issuer, storage) + + server := &http.Server{ + Addr: ":" + port, + Handler: router, + } + log.Printf("server listening on http://localhost:%s/", port) + log.Println("press ctrl+c to stop") + err := server.ListenAndServe() + if err != nil { + log.Fatal(err) + } +} diff --git a/auth/op/device.go b/auth/op/device.go new file mode 100644 index 0000000..65da90b --- /dev/null +++ b/auth/op/device.go @@ -0,0 +1,191 @@ +package op + +import ( + "errors" + "fmt" + "io" + "net/http" + "net/url" + + "github.com/gorilla/mux" + "github.com/gorilla/securecookie" + "github.com/sirupsen/logrus" + "github.com/zitadel/oidc/v2/pkg/op" +) + +type deviceAuthenticate interface { + CheckUsernamePasswordSimple(username, password string) error + op.DeviceAuthorizationStorage +} + +type deviceLogin struct { + storage deviceAuthenticate + cookie *securecookie.SecureCookie +} + +func registerDeviceAuth(storage deviceAuthenticate, router *mux.Router) { + l := &deviceLogin{ + storage: storage, + cookie: securecookie.New(securecookie.GenerateRandomKey(32), nil), + } + + router.HandleFunc("", l.userCodeHandler) + router.Path("/login").Methods(http.MethodPost).HandlerFunc(l.loginHandler) + router.HandleFunc("/confirm", l.confirmHandler) +} + +func renderUserCode(w io.Writer, err error) { + data := struct { + Error string + }{ + Error: errMsg(err), + } + + if err := templates.ExecuteTemplate(w, "usercode", data); err != nil { + logrus.Error(err) + } +} + +func renderDeviceLogin(w http.ResponseWriter, userCode string, err error) { + data := &struct { + UserCode string + Error string + }{ + UserCode: userCode, + Error: errMsg(err), + } + if err = templates.ExecuteTemplate(w, "device_login", data); err != nil { + logrus.Error(err) + } +} + +func renderConfirmPage(w http.ResponseWriter, username, clientID string, scopes []string) { + data := &struct { + Username string + ClientID string + Scopes []string + }{ + Username: username, + ClientID: clientID, + Scopes: scopes, + } + if err := templates.ExecuteTemplate(w, "confirm_device", data); err != nil { + logrus.Error(err) + } +} + +func (d *deviceLogin) userCodeHandler(w http.ResponseWriter, r *http.Request) { + err := r.ParseForm() + if err != nil { + w.WriteHeader(http.StatusBadRequest) + renderUserCode(w, err) + return + } + userCode := r.Form.Get("user_code") + if userCode == "" { + if prompt, _ := url.QueryUnescape(r.Form.Get("prompt")); prompt != "" { + err = errors.New(prompt) + } + renderUserCode(w, err) + return + } + + renderDeviceLogin(w, userCode, nil) +} + +func redirectBack(w http.ResponseWriter, r *http.Request, prompt string) { + values := make(url.Values) + values.Set("prompt", url.QueryEscape(prompt)) + + url := url.URL{ + Path: "/device", + RawQuery: values.Encode(), + } + http.Redirect(w, r, url.String(), http.StatusSeeOther) +} + +const userCodeCookieName = "user_code" + +type userCodeCookie struct { + UserCode string + UserName string +} + +func (d *deviceLogin) loginHandler(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + redirectBack(w, r, err.Error()) + return + } + + userCode := r.PostForm.Get("user_code") + if userCode == "" { + redirectBack(w, r, "missing user_code in request") + return + } + username := r.PostForm.Get("username") + if username == "" { + redirectBack(w, r, "missing username in request") + return + } + password := r.PostForm.Get("password") + if password == "" { + redirectBack(w, r, "missing password in request") + return + } + + if err := d.storage.CheckUsernamePasswordSimple(username, password); err != nil { + redirectBack(w, r, err.Error()) + return + } + state, err := d.storage.GetDeviceAuthorizationByUserCode(r.Context(), userCode) + if err != nil { + redirectBack(w, r, err.Error()) + return + } + + encoded, err := d.cookie.Encode(userCodeCookieName, userCodeCookie{userCode, username}) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + cookie := &http.Cookie{ + Name: userCodeCookieName, + Value: encoded, + Path: "/", + } + http.SetCookie(w, cookie) + renderConfirmPage(w, username, state.ClientID, state.Scopes) +} + +func (d *deviceLogin) confirmHandler(w http.ResponseWriter, r *http.Request) { + cookie, err := r.Cookie(userCodeCookieName) + if err != nil { + redirectBack(w, r, err.Error()) + return + } + data := new(userCodeCookie) + if err = d.cookie.Decode(userCodeCookieName, cookie.Value, &data); err != nil { + redirectBack(w, r, err.Error()) + return + } + if err = r.ParseForm(); err != nil { + redirectBack(w, r, err.Error()) + return + } + + action := r.Form.Get("action") + switch action { + case "allowed": + err = d.storage.CompleteDeviceAuthorization(r.Context(), data.UserCode, data.UserName) + case "denied": + err = d.storage.DenyDeviceAuthorization(r.Context(), data.UserCode) + default: + err = errors.New("action must be one of \"allow\" or \"deny\"") + } + if err != nil { + redirectBack(w, r, err.Error()) + return + } + + fmt.Fprintf(w, "Device authorization %s. You can now return to the device", action) +} diff --git a/auth/op/login.go b/auth/op/login.go new file mode 100644 index 0000000..090438a --- /dev/null +++ b/auth/op/login.go @@ -0,0 +1,77 @@ +package op + +import ( + "context" + "fmt" + "net/http" + + "github.com/gorilla/mux" + "github.com/zitadel/oidc/v2/pkg/op" +) + +type login struct { + authenticate authenticate + router *mux.Router + callback func(context.Context, string) string +} + +func NewLogin(authenticate authenticate, callback func(context.Context, string) string, issuerInterceptor *op.IssuerInterceptor) *login { + l := &login{ + authenticate: authenticate, + callback: callback, + } + l.createRouter(issuerInterceptor) + return l +} + +func (l *login) createRouter(issuerInterceptor *op.IssuerInterceptor) { + l.router = mux.NewRouter() + l.router.Path("/username").Methods("GET").HandlerFunc(l.loginHandler) + l.router.Path("/username").Methods("POST").HandlerFunc(issuerInterceptor.HandlerFunc(l.checkLoginHandler)) +} + +type authenticate interface { + CheckUsernamePassword(username, password, id string) error +} + +func (l *login) loginHandler(w http.ResponseWriter, r *http.Request) { + err := r.ParseForm() + if err != nil { + http.Error(w, fmt.Sprintf("cannot parse form:%s", err), http.StatusInternalServerError) + return + } + // the oidc package will pass the id of the auth request as query parameter + // we will use this id through the login process and therefore pass it to the login page + renderLogin(w, r.FormValue(queryAuthRequestID), nil) +} + +func renderLogin(w http.ResponseWriter, id string, err error) { + data := &struct { + ID string + Error string + }{ + ID: id, + Error: errMsg(err), + } + err = templates.ExecuteTemplate(w, "login", data) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +func (l *login) checkLoginHandler(w http.ResponseWriter, r *http.Request) { + err := r.ParseForm() + if err != nil { + http.Error(w, fmt.Sprintf("cannot parse form:%s", err), http.StatusInternalServerError) + return + } + username := r.FormValue("username") + password := r.FormValue("password") + id := r.FormValue("id") + err = l.authenticate.CheckUsernamePassword(username, password, id) + if err != nil { + renderLogin(w, id, err) + return + } + http.Redirect(w, r, l.callback(r.Context(), id), http.StatusFound) +} diff --git a/auth/op/op.go b/auth/op/op.go new file mode 100644 index 0000000..1f87c66 --- /dev/null +++ b/auth/op/op.go @@ -0,0 +1,127 @@ +package op + +import ( + "crypto/sha256" + "log" + "net/http" + "time" + + "github.com/gorilla/mux" + "golang.org/x/text/language" + + "auth/storage" + "github.com/zitadel/oidc/v2/pkg/op" +) + +const ( + pathLoggedOut = "/logged-out" +) + +func init() { + storage.RegisterClients( + storage.NativeClient("native"), + storage.WebClient("web", "secret"), + storage.WebClient("api", "secret"), + ) +} + +type Storage interface { + op.Storage + authenticate + deviceAuthenticate +} + +// SetupServer creates an OIDC server with Issuer=http://localhost: +// +// Use one of the pre-made clients in storage/clients.go or register a new one. +func SetupServer(issuer string, storage Storage, extraOptions ...op.Option) *mux.Router { + // the OpenID Provider requires a 32-byte key for (token) encryption + // be sure to create a proper crypto random key and manage it securely! + key := sha256.Sum256([]byte("test")) + + router := mux.NewRouter() + + // for simplicity, we provide a very small default page for users who have signed out + router.HandleFunc(pathLoggedOut, func(w http.ResponseWriter, req *http.Request) { + _, err := w.Write([]byte("signed out successfully")) + if err != nil { + log.Printf("error serving logged out page: %v", err) + } + }) + + // creation of the OpenIDProvider with the just created in-memory Storage + provider, err := newOP(storage, issuer, key, extraOptions...) + if err != nil { + log.Fatal(err) + } + + //the provider will only take care of the OpenID Protocol, so there must be some sort of UI for the login process + //for the simplicity of the example this means a simple page with username and password field + //be sure to provide an IssuerInterceptor with the IssuerFromRequest from the OP so the login can select / and pass it to the storage + l := NewLogin(storage, op.AuthCallbackURL(provider), op.NewIssuerInterceptor(provider.IssuerFromRequest)) + + // regardless of how many pages / steps there are in the process, the UI must be registered in the router, + // so we will direct all calls to /login to the login UI + router.PathPrefix("/login/").Handler(http.StripPrefix("/login", l.router)) + + router.PathPrefix("/device").Subrouter() + registerDeviceAuth(storage, router.PathPrefix("/device").Subrouter()) + + // we register the http handler of the OP on the root, so that the discovery endpoint (/.well-known/openid-configuration) + // is served on the correct path + // + // if your issuer ends with a path (e.g. http://localhost:9998/custom/path/), + // then you would have to set the path prefix (/custom/path/) + router.PathPrefix("/").Handler(provider.HttpHandler()) + + return router +} + +// newOP will create an OpenID Provider for localhost on a specified port with a given encryption key +// and a predefined default logout uri +// it will enable all options (see descriptions) +func newOP(storage op.Storage, issuer string, key [32]byte, extraOptions ...op.Option) (op.OpenIDProvider, error) { + config := &op.Config{ + CryptoKey: key, + + // will be used if the end_session endpoint is called without a post_logout_redirect_uri + DefaultLogoutRedirectURI: pathLoggedOut, + + // enables code_challenge_method S256 for PKCE (and therefore PKCE in general) + CodeMethodS256: true, + + // enables additional client_id/client_secret authentication by form post (not only HTTP Basic Auth) + AuthMethodPost: true, + + // enables additional authentication by using private_key_jwt + AuthMethodPrivateKeyJWT: true, + + // enables refresh_token grant use + GrantTypeRefreshToken: true, + + // enables use of the `request` Object parameter + RequestObjectSupported: true, + + // this example has only static texts (in English), so we'll set the here accordingly + SupportedUILocales: []language.Tag{language.English}, + + DeviceAuthorization: op.DeviceAuthorizationConfig{ + Lifetime: 5 * time.Minute, + PollInterval: 5 * time.Second, + UserFormPath: "/device", + UserCode: op.UserCodeBase20, + }, + } + handler, err := op.NewOpenIDProvider(issuer, config, storage, + append([]op.Option{ + // we must explicitly allow the use of the http issuer + op.WithAllowInsecure(), + // as an example on how to customize an endpoint this will change the authorization_endpoint from /authorize to /auth + op.WithCustomAuthEndpoint(op.NewEndpoint("auth")), + }, extraOptions...)..., + ) + if err != nil { + return nil, err + } + return handler, nil +} diff --git a/auth/op/templates.go b/auth/op/templates.go new file mode 100644 index 0000000..da8cc27 --- /dev/null +++ b/auth/op/templates.go @@ -0,0 +1,26 @@ +package op + +import ( + "embed" + "html/template" + + "github.com/sirupsen/logrus" +) + +var ( + //go:embed templates + templateFS embed.FS + templates = template.Must(template.ParseFS(templateFS, "templates/*.html")) +) + +const ( + queryAuthRequestID = "authRequestID" +) + +func errMsg(err error) string { + if err == nil { + return "" + } + logrus.Error(err) + return err.Error() +} diff --git a/auth/op/templates/confirm_device.html b/auth/op/templates/confirm_device.html new file mode 100644 index 0000000..a6bcdad --- /dev/null +++ b/auth/op/templates/confirm_device.html @@ -0,0 +1,25 @@ +{{ define "confirm_device" -}} + + + + + Confirm device authorization + + + +

Welcome back {{.Username}}!

+

+ You are about to grant device {{.ClientID}} access to the following scopes: {{.Scopes}}. +

+ + + + +{{- end }} diff --git a/auth/op/templates/device_login.html b/auth/op/templates/device_login.html new file mode 100644 index 0000000..cc5b00b --- /dev/null +++ b/auth/op/templates/device_login.html @@ -0,0 +1,29 @@ +{{ define "device_login" -}} + + + + + Login + + +
+ + + +
+ + +
+ +
+ + +
+ +

{{.Error}}

+ + +
+ + +{{- end }} diff --git a/auth/op/templates/login.html b/auth/op/templates/login.html new file mode 100644 index 0000000..b048211 --- /dev/null +++ b/auth/op/templates/login.html @@ -0,0 +1,29 @@ +{{ define "login" -}} + + + + + Login + + +
+ + + +
+ + +
+ +
+ + +
+ +

{{.Error}}

+ + +
+ +` +{{- end }} \ No newline at end of file diff --git a/auth/op/templates/usercode.html b/auth/op/templates/usercode.html new file mode 100644 index 0000000..fb8fa7f --- /dev/null +++ b/auth/op/templates/usercode.html @@ -0,0 +1,21 @@ +{{ define "usercode" -}} + + + + + Device authorization + + +
+

Device authorization

+
+ + +
+

{{.Error}}

+ + +
+ + +{{- end }} diff --git a/auth/resources/service-key1.json b/auth/resources/service-key1.json new file mode 100644 index 0000000..a0d20e8 --- /dev/null +++ b/auth/resources/service-key1.json @@ -0,0 +1 @@ +{"type":"serviceaccount","keyId":"key1","key":"-----BEGIN RSA PRIVATE KEY-----\nMIICXgIBAAKBgQD21E+180rCAzp15zy2X/JOYYHtxYhF51pWCsITeChJd7sFWxp1\ntxSHTiomQYBiBWgcCavsdu/VLPQJhO3PTIyglxc1XRGsM48oDT5MkFsAVDvbjuWk\nF0lstQyw4pr8Wg0Ecf1aL6YlvVKB9h5rAgZ9T+elNJ7q5takMAvNhu7zMQIDAQAB\nAoGAeLRw2qjEaUZM43WWchVPmFcEw/MyZgTyX1tZd03uXacolUDtGp3ScyydXiHw\nF39PX063fabYOCaInNMdvJ9RsQz2OcZuS/K6NOmWhzBfLgs4Y1tU6ijoY/gBjHgu\nCV0KjvoWIfEtKl/On/wTrAnUStFzrc7U4dpKFP1fy2ZTTnECQQD8aP2QOxmKUyfg\nBAjfonpkrNeaTRNwTULTvEHFiLyaeFd1PAvsDiKZtpk6iHLb99mQZkVVtAK5qgQ4\n1OI72jkVAkEA+lcAamuZAM+gIiUhbHA7BfX9OVgyGDD2tx5g/kxhMUmK6hIiO6Ul\n0nw5KfrCEUU3AzrM7HejUg3q61SYcXTgrQJBALhrzbhwNf0HPP9Ec2dSw7KDRxSK\ndEV9bfJefn/hpEwI2X3i3aMfwNAmxlYqFCH8OY5z6vzvhX46ZtNPV+z7SPECQQDq\nApXi5P27YlpgULEzup2R7uZsymLZdjvJ5V3pmOBpwENYlublNnVqkrCk60CqADdy\nj26rxRIoS9ZDcWqm9AhpAkEAyrNXBMJh08ghBMb3NYPFfr/bftRJSrGjhBPuJ5qr\nXzWaXhYVMMh3OSAwzHBJbA1ffdQJuH2ebL99Ur5fpBcbVw==\n-----END RSA PRIVATE KEY-----\n","userId":"service"} diff --git a/auth/storage/client.go b/auth/storage/client.go new file mode 100644 index 0000000..d92efd4 --- /dev/null +++ b/auth/storage/client.go @@ -0,0 +1,241 @@ +package storage + +import ( + "time" + + "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v2/pkg/op" +) + +var ( + // we use the default login UI and pass the (auth request) id + defaultLoginURL = func(id string) string { + return "/login/username?authRequestID=" + id + } + + // clients to be used by the storage interface + clients = map[string]*Client{} +) + +// Client represents the storage model of an OAuth/OIDC client +// this could also be your database model +type Client struct { + id string + secret string + redirectURIs []string + applicationType op.ApplicationType + authMethod oidc.AuthMethod + loginURL func(string) string + responseTypes []oidc.ResponseType + grantTypes []oidc.GrantType + accessTokenType op.AccessTokenType + devMode bool + idTokenUserinfoClaimsAssertion bool + clockSkew time.Duration + postLogoutRedirectURIGlobs []string + redirectURIGlobs []string +} + +// GetID must return the client_id +func (c *Client) GetID() string { + return c.id +} + +// RedirectURIs must return the registered redirect_uris for Code and Implicit Flow +func (c *Client) RedirectURIs() []string { + return c.redirectURIs +} + +// PostLogoutRedirectURIs must return the registered post_logout_redirect_uris for sign-outs +func (c *Client) PostLogoutRedirectURIs() []string { + return []string{} +} + +// ApplicationType must return the type of the client (app, native, user agent) +func (c *Client) ApplicationType() op.ApplicationType { + return c.applicationType +} + +// AuthMethod must return the authentication method (client_secret_basic, client_secret_post, none, private_key_jwt) +func (c *Client) AuthMethod() oidc.AuthMethod { + return c.authMethod +} + +// ResponseTypes must return all allowed response types (code, id_token token, id_token) +// these must match with the allowed grant types +func (c *Client) ResponseTypes() []oidc.ResponseType { + return c.responseTypes +} + +// GrantTypes must return all allowed grant types (authorization_code, refresh_token, urn:ietf:params:oauth:grant-type:jwt-bearer) +func (c *Client) GrantTypes() []oidc.GrantType { + return c.grantTypes +} + +// LoginURL will be called to redirect the user (agent) to the login UI +// you could implement some logic here to redirect the users to different login UIs depending on the client +func (c *Client) LoginURL(id string) string { + return c.loginURL(id) +} + +// AccessTokenType must return the type of access token the client uses (Bearer (opaque) or JWT) +func (c *Client) AccessTokenType() op.AccessTokenType { + return c.accessTokenType +} + +// IDTokenLifetime must return the lifetime of the client's id_tokens +func (c *Client) IDTokenLifetime() time.Duration { + return 1 * time.Hour +} + +// DevMode enables the use of non-compliant configs such as redirect_uris (e.g. http schema for user agent client) +func (c *Client) DevMode() bool { + return c.devMode +} + +// RestrictAdditionalIdTokenScopes allows specifying which custom scopes shall be asserted into the id_token +func (c *Client) RestrictAdditionalIdTokenScopes() func(scopes []string) []string { + return func(scopes []string) []string { + return scopes + } +} + +// RestrictAdditionalAccessTokenScopes allows specifying which custom scopes shall be asserted into the JWT access_token +func (c *Client) RestrictAdditionalAccessTokenScopes() func(scopes []string) []string { + return func(scopes []string) []string { + return scopes + } +} + +// IsScopeAllowed enables Client specific custom scopes validation +// in this example we allow the CustomScope for all clients +func (c *Client) IsScopeAllowed(scope string) bool { + return scope == CustomScope +} + +// IDTokenUserinfoClaimsAssertion allows specifying if claims of scope profile, email, phone and address are asserted into the id_token +// even if an access token if issued which violates the OIDC Core spec +// (5.4. Requesting Claims using Scope Values: https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims) +// some clients though require that e.g. email is always in the id_token when requested even if an access_token is issued +func (c *Client) IDTokenUserinfoClaimsAssertion() bool { + return c.idTokenUserinfoClaimsAssertion +} + +// ClockSkew enables clients to instruct the OP to apply a clock skew on the various times and expirations +// (subtract from issued_at, add to expiration, ...) +func (c *Client) ClockSkew() time.Duration { + return c.clockSkew +} + +// RegisterClients enables you to register clients for the example implementation +// there are some clients (web and native) to try out different cases +// add more if necessary +// +// RegisterClients should be called before the Storage is used so that there are +// no race conditions. +func RegisterClients(registerClients ...*Client) { + for _, client := range registerClients { + clients[client.id] = client + } +} + +// NativeClient will create a client of type native, which will always use PKCE and allow the use of refresh tokens +// user-defined redirectURIs may include: +// - http://localhost without port specification (e.g. http://localhost/auth/callback) +// - custom protocol (e.g. custom://auth/callback) +// (the examples will be used as default, if none is provided) +func NativeClient(id string, redirectURIs ...string) *Client { + if len(redirectURIs) == 0 { + redirectURIs = []string{ + "http://localhost/auth/callback", + "http://localhost:8080/login/callback", + "http://localhost:5000/login/callback", + "http://localhost:5173/login/callback", + "custom://auth/callback", + } + } + return &Client{ + id: id, + secret: "", // no secret needed (due to PKCE) + redirectURIs: redirectURIs, + applicationType: op.ApplicationTypeNative, + authMethod: oidc.AuthMethodNone, + loginURL: defaultLoginURL, + responseTypes: []oidc.ResponseType{oidc.ResponseTypeCode}, + grantTypes: []oidc.GrantType{oidc.GrantTypeCode, oidc.GrantTypeRefreshToken}, + accessTokenType: op.AccessTokenTypeBearer, + devMode: false, + idTokenUserinfoClaimsAssertion: false, + clockSkew: 0, + } +} + +// WebClient will create a client of type web, which will always use Basic Auth and allow the use of refresh tokens +// user-defined redirectURIs may include: +// - http://localhost with port specification (e.g. http://localhost:9999/auth/callback) +// (the example will be used as default, if none is provided) +func WebClient(id, secret string, redirectURIs ...string) *Client { + if len(redirectURIs) == 0 { + redirectURIs = []string{ + "http://localhost:5000/login/callback", + "http://localhost:9999/auth/callback", + "http://localhost:8080/login/callback", + "http://localhost:5173/login/callback", + } + } + return &Client{ + id: id, + secret: secret, + redirectURIs: redirectURIs, + applicationType: op.ApplicationTypeWeb, + authMethod: oidc.AuthMethodBasic, + loginURL: defaultLoginURL, + responseTypes: []oidc.ResponseType{oidc.ResponseTypeCode}, + grantTypes: []oidc.GrantType{oidc.GrantTypeCode, oidc.GrantTypeRefreshToken}, + accessTokenType: op.AccessTokenTypeBearer, + devMode: false, + idTokenUserinfoClaimsAssertion: false, + clockSkew: 0, + } +} + +// DeviceClient creates a device client with Basic authentication. +func DeviceClient(id, secret string) *Client { + return &Client{ + id: id, + secret: secret, + redirectURIs: nil, + applicationType: op.ApplicationTypeWeb, + authMethod: oidc.AuthMethodBasic, + loginURL: defaultLoginURL, + responseTypes: []oidc.ResponseType{oidc.ResponseTypeCode}, + grantTypes: []oidc.GrantType{oidc.GrantTypeDeviceCode}, + accessTokenType: op.AccessTokenTypeBearer, + devMode: false, + idTokenUserinfoClaimsAssertion: false, + clockSkew: 0, + } +} + +type hasRedirectGlobs struct { + *Client +} + +// RedirectURIGlobs provide wildcarding for additional valid redirects +func (c hasRedirectGlobs) RedirectURIGlobs() []string { + return c.redirectURIGlobs +} + +// PostLogoutRedirectURIGlobs provide extra wildcarding for additional valid redirects +func (c hasRedirectGlobs) PostLogoutRedirectURIGlobs() []string { + return c.postLogoutRedirectURIGlobs +} + +// RedirectGlobsClient wraps the client in a op.HasRedirectGlobs +// only if DevMode is enabled. +func RedirectGlobsClient(client *Client) op.Client { + if client.devMode { + return hasRedirectGlobs{client} + } + return client +} diff --git a/auth/storage/oidc.go b/auth/storage/oidc.go new file mode 100644 index 0000000..f5412cf --- /dev/null +++ b/auth/storage/oidc.go @@ -0,0 +1,205 @@ +package storage + +import ( + "time" + + "golang.org/x/text/language" + + "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v2/pkg/op" +) + +const ( + // CustomScope is an example for how to use custom scopes in this library + //(in this scenario, when requested, it will return a custom claim) + CustomScope = "custom_scope" + + // CustomClaim is an example for how to return custom claims with this library + CustomClaim = "custom_claim" + + // CustomScopeImpersonatePrefix is an example scope prefix for passing user id to impersonate using token exchage + CustomScopeImpersonatePrefix = "custom_scope:impersonate:" +) + +type AuthRequest struct { + ID string + CreationDate time.Time + ApplicationID string + CallbackURI string + TransferState string + Prompt []string + UiLocales []language.Tag + LoginHint string + MaxAuthAge *time.Duration + UserID string + Scopes []string + ResponseType oidc.ResponseType + Nonce string + CodeChallenge *OIDCCodeChallenge + + done bool + authTime time.Time +} + +func (a *AuthRequest) GetID() string { + return a.ID +} + +func (a *AuthRequest) GetACR() string { + return "" // we won't handle acr in this example +} + +func (a *AuthRequest) GetAMR() []string { + // this example only uses password for authentication + if a.done { + return []string{"pwd"} + } + return nil +} + +func (a *AuthRequest) GetAudience() []string { + return []string{a.ApplicationID} // this example will always just use the client_id as audience +} + +func (a *AuthRequest) GetAuthTime() time.Time { + return a.authTime +} + +func (a *AuthRequest) GetClientID() string { + return a.ApplicationID +} + +func (a *AuthRequest) GetCodeChallenge() *oidc.CodeChallenge { + return CodeChallengeToOIDC(a.CodeChallenge) +} + +func (a *AuthRequest) GetNonce() string { + return a.Nonce +} + +func (a *AuthRequest) GetRedirectURI() string { + return a.CallbackURI +} + +func (a *AuthRequest) GetResponseType() oidc.ResponseType { + return a.ResponseType +} + +func (a *AuthRequest) GetResponseMode() oidc.ResponseMode { + return "" // we won't handle response mode in this example +} + +func (a *AuthRequest) GetScopes() []string { + return a.Scopes +} + +func (a *AuthRequest) GetState() string { + return a.TransferState +} + +func (a *AuthRequest) GetSubject() string { + return a.UserID +} + +func (a *AuthRequest) Done() bool { + return a.done +} + +func PromptToInternal(oidcPrompt oidc.SpaceDelimitedArray) []string { + prompts := make([]string, len(oidcPrompt)) + for _, oidcPrompt := range oidcPrompt { + switch oidcPrompt { + case oidc.PromptNone, + oidc.PromptLogin, + oidc.PromptConsent, + oidc.PromptSelectAccount: + prompts = append(prompts, oidcPrompt) + } + } + return prompts +} + +func MaxAgeToInternal(maxAge *uint) *time.Duration { + if maxAge == nil { + return nil + } + dur := time.Duration(*maxAge) * time.Second + return &dur +} + +func authRequestToInternal(authReq *oidc.AuthRequest, userID string) *AuthRequest { + return &AuthRequest{ + CreationDate: time.Now(), + ApplicationID: authReq.ClientID, + CallbackURI: authReq.RedirectURI, + TransferState: authReq.State, + Prompt: PromptToInternal(authReq.Prompt), + UiLocales: authReq.UILocales, + LoginHint: authReq.LoginHint, + MaxAuthAge: MaxAgeToInternal(authReq.MaxAge), + UserID: userID, + Scopes: authReq.Scopes, + ResponseType: authReq.ResponseType, + Nonce: authReq.Nonce, + CodeChallenge: &OIDCCodeChallenge{ + Challenge: authReq.CodeChallenge, + Method: string(authReq.CodeChallengeMethod), + }, + } +} + +type OIDCCodeChallenge struct { + Challenge string + Method string +} + +func CodeChallengeToOIDC(challenge *OIDCCodeChallenge) *oidc.CodeChallenge { + if challenge == nil { + return nil + } + challengeMethod := oidc.CodeChallengeMethodPlain + if challenge.Method == "S256" { + challengeMethod = oidc.CodeChallengeMethodS256 + } + return &oidc.CodeChallenge{ + Challenge: challenge.Challenge, + Method: challengeMethod, + } +} + +// RefreshTokenRequestFromBusiness will simply wrap the storage RefreshToken to implement the op.RefreshTokenRequest interface +func RefreshTokenRequestFromBusiness(token *RefreshToken) op.RefreshTokenRequest { + return &RefreshTokenRequest{token} +} + +type RefreshTokenRequest struct { + *RefreshToken +} + +func (r *RefreshTokenRequest) GetAMR() []string { + return r.AMR +} + +func (r *RefreshTokenRequest) GetAudience() []string { + return r.Audience +} + +func (r *RefreshTokenRequest) GetAuthTime() time.Time { + return r.AuthTime +} + +func (r *RefreshTokenRequest) GetClientID() string { + return r.ApplicationID +} + +func (r *RefreshTokenRequest) GetScopes() []string { + return r.Scopes +} + +func (r *RefreshTokenRequest) GetSubject() string { + return r.UserID +} + +func (r *RefreshTokenRequest) SetCurrentScopes(scopes []string) { + r.Scopes = scopes +} diff --git a/auth/storage/storage.go b/auth/storage/storage.go new file mode 100644 index 0000000..0b09c1d --- /dev/null +++ b/auth/storage/storage.go @@ -0,0 +1,917 @@ +package storage + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "errors" + "fmt" + "math/big" + "strings" + "sync" + "time" + + "github.com/google/uuid" + "gopkg.in/square/go-jose.v2" + + "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v2/pkg/op" +) + +// serviceKey1 is a public key which will be used for the JWT Profile Authorization Grant +// the corresponding private key is in the service-key1.json (for demonstration purposes) +var serviceKey1 = &rsa.PublicKey{ + N: func() *big.Int { + n, _ := new(big.Int).SetString("00f6d44fb5f34ac2033a75e73cb65ff24e6181edc58845e75a560ac21378284977bb055b1a75b714874e2a2641806205681c09abec76efd52cf40984edcf4c8ca09717355d11ac338f280d3e4c905b00543bdb8ee5a417496cb50cb0e29afc5a0d0471fd5a2fa625bd5281f61e6b02067d4fe7a5349eeae6d6a4300bcd86eef331", 16) + return n + }(), + E: 65537, +} + +var ( + _ op.Storage = &Storage{} + _ op.ClientCredentialsStorage = &Storage{} +) + +// storage implements the op.Storage interface +// typically you would implement this as a layer on top of your database +// for simplicity this example keeps everything in-memory +type Storage struct { + lock sync.Mutex + authRequests map[string]*AuthRequest + codes map[string]string + tokens map[string]*Token + clients map[string]*Client + userStore UserStore + services map[string]Service + refreshTokens map[string]*RefreshToken + signingKey signingKey + deviceCodes map[string]deviceAuthorizationEntry + userCodes map[string]string + serviceUsers map[string]*Client +} + +type signingKey struct { + id string + algorithm jose.SignatureAlgorithm + key *rsa.PrivateKey +} + +func (s *signingKey) SignatureAlgorithm() jose.SignatureAlgorithm { + return s.algorithm +} + +func (s *signingKey) Key() interface{} { + return s.key +} + +func (s *signingKey) ID() string { + return s.id +} + +type publicKey struct { + signingKey +} + +func (s *publicKey) ID() string { + return s.id +} + +func (s *publicKey) Algorithm() jose.SignatureAlgorithm { + return s.algorithm +} + +func (s *publicKey) Use() string { + return "sig" +} + +func (s *publicKey) Key() interface{} { + return &s.key.PublicKey +} + +func NewStorage(userStore UserStore) *Storage { + key, _ := rsa.GenerateKey(rand.Reader, 2048) + return &Storage{ + authRequests: make(map[string]*AuthRequest), + codes: make(map[string]string), + tokens: make(map[string]*Token), + refreshTokens: make(map[string]*RefreshToken), + clients: clients, + userStore: userStore, + services: map[string]Service{ + userStore.ExampleClientID(): { + keys: map[string]*rsa.PublicKey{ + "key1": serviceKey1, + }, + }, + }, + signingKey: signingKey{ + id: uuid.NewString(), + algorithm: jose.RS256, + key: key, + }, + deviceCodes: make(map[string]deviceAuthorizationEntry), + userCodes: make(map[string]string), + serviceUsers: map[string]*Client{ + "sid1": { + id: "sid1", + secret: "verysecret", + grantTypes: []oidc.GrantType{ + oidc.GrantTypeClientCredentials, + }, + accessTokenType: op.AccessTokenTypeBearer, + }, + }, + } +} + +// CheckUsernamePassword implements the `authenticate` interface of the login +func (s *Storage) CheckUsernamePassword(username, password, id string) error { + s.lock.Lock() + defer s.lock.Unlock() + request, ok := s.authRequests[id] + if !ok { + return fmt.Errorf("request not found") + } + + // for demonstration purposes we'll check we'll have a simple user store and + // a plain text password. For real world scenarios, be sure to have the password + // hashed and salted (e.g. using bcrypt) + user := s.userStore.GetUserByUsername(username) + if user != nil && user.Password == password { + // be sure to set user id into the auth request after the user was checked, + // so that you'll be able to get more information about the user after the login + request.UserID = user.ID + + // you will have to change some state on the request to guide the user through possible multiple steps of the login process + // in this example we'll simply check the username / password and set a boolean to true + // therefore we will also just check this boolean if the request / login has been finished + request.done = true + return nil + } + return fmt.Errorf("username or password wrong") +} + +func (s *Storage) CheckUsernamePasswordSimple(username, password string) error { + s.lock.Lock() + defer s.lock.Unlock() + + user := s.userStore.GetUserByUsername(username) + if user != nil && user.Password == password { + return nil + } + return fmt.Errorf("username or password wrong") +} + +// CreateAuthRequest implements the op.Storage interface +// it will be called after parsing and validation of the authentication request +func (s *Storage) CreateAuthRequest(ctx context.Context, authReq *oidc.AuthRequest, userID string) (op.AuthRequest, error) { + s.lock.Lock() + defer s.lock.Unlock() + + if len(authReq.Prompt) == 1 && authReq.Prompt[0] == "none" { + // With prompt=none, there is no way for the user to log in + // so return error right away. + return nil, oidc.ErrLoginRequired() + } + + // typically, you'll fill your storage / storage model with the information of the passed object + request := authRequestToInternal(authReq, userID) + + // you'll also have to create a unique id for the request (this might be done by your database; we'll use a uuid) + request.ID = uuid.NewString() + + // and save it in your database (for demonstration purposed we will use a simple map) + s.authRequests[request.ID] = request + + // finally, return the request (which implements the AuthRequest interface of the OP + return request, nil +} + +// AuthRequestByID implements the op.Storage interface +// it will be called after the Login UI redirects back to the OIDC endpoint +func (s *Storage) AuthRequestByID(ctx context.Context, id string) (op.AuthRequest, error) { + s.lock.Lock() + defer s.lock.Unlock() + request, ok := s.authRequests[id] + if !ok { + return nil, fmt.Errorf("request not found") + } + return request, nil +} + +// AuthRequestByCode implements the op.Storage interface +// it will be called after parsing and validation of the token request (in an authorization code flow) +func (s *Storage) AuthRequestByCode(ctx context.Context, code string) (op.AuthRequest, error) { + // for this example we read the id by code and then get the request by id + requestID, ok := func() (string, bool) { + s.lock.Lock() + defer s.lock.Unlock() + requestID, ok := s.codes[code] + return requestID, ok + }() + if !ok { + return nil, fmt.Errorf("code invalid or expired") + } + return s.AuthRequestByID(ctx, requestID) +} + +// SaveAuthCode implements the op.Storage interface +// it will be called after the authentication has been successful and before redirecting the user agent to the redirect_uri +// (in an authorization code flow) +func (s *Storage) SaveAuthCode(ctx context.Context, id string, code string) error { + // for this example we'll just save the authRequestID to the code + s.lock.Lock() + defer s.lock.Unlock() + s.codes[code] = id + return nil +} + +// DeleteAuthRequest implements the op.Storage interface +// it will be called after creating the token response (id and access tokens) for a valid +// - authentication request (in an implicit flow) +// - token request (in an authorization code flow) +func (s *Storage) DeleteAuthRequest(ctx context.Context, id string) error { + // you can simply delete all reference to the auth request + s.lock.Lock() + defer s.lock.Unlock() + delete(s.authRequests, id) + for code, requestID := range s.codes { + if id == requestID { + delete(s.codes, code) + return nil + } + } + return nil +} + +// CreateAccessToken implements the op.Storage interface +// it will be called for all requests able to return an access token (Authorization Code Flow, Implicit Flow, JWT Profile, ...) +func (s *Storage) CreateAccessToken(ctx context.Context, request op.TokenRequest) (string, time.Time, error) { + var applicationID string + switch req := request.(type) { + case *AuthRequest: + // if authenticated for an app (auth code / implicit flow) we must save the client_id to the token + applicationID = req.ApplicationID + case op.TokenExchangeRequest: + applicationID = req.GetClientID() + } + + token, err := s.accessToken(applicationID, "", request.GetSubject(), request.GetAudience(), request.GetScopes()) + if err != nil { + return "", time.Time{}, err + } + return token.ID, token.Expiration, nil +} + +// CreateAccessAndRefreshTokens implements the op.Storage interface +// it will be called for all requests able to return an access and refresh token (Authorization Code Flow, Refresh Token Request) +func (s *Storage) CreateAccessAndRefreshTokens(ctx context.Context, request op.TokenRequest, currentRefreshToken string) (accessTokenID string, newRefreshToken string, expiration time.Time, err error) { + // generate tokens via token exchange flow if request is relevant + if teReq, ok := request.(op.TokenExchangeRequest); ok { + return s.exchangeRefreshToken(ctx, teReq) + } + + // get the information depending on the request type / implementation + applicationID, authTime, amr := getInfoFromRequest(request) + + // if currentRefreshToken is empty (Code Flow) we will have to create a new refresh token + if currentRefreshToken == "" { + refreshTokenID := uuid.NewString() + accessToken, err := s.accessToken(applicationID, refreshTokenID, request.GetSubject(), request.GetAudience(), request.GetScopes()) + if err != nil { + return "", "", time.Time{}, err + } + refreshToken, err := s.createRefreshToken(accessToken, amr, authTime) + if err != nil { + return "", "", time.Time{}, err + } + return accessToken.ID, refreshToken, accessToken.Expiration, nil + } + + // if we get here, the currentRefreshToken was not empty, so the call is a refresh token request + // we therefore will have to check the currentRefreshToken and renew the refresh token + refreshToken, refreshTokenID, err := s.renewRefreshToken(currentRefreshToken) + if err != nil { + return "", "", time.Time{}, err + } + accessToken, err := s.accessToken(applicationID, refreshTokenID, request.GetSubject(), request.GetAudience(), request.GetScopes()) + if err != nil { + return "", "", time.Time{}, err + } + return accessToken.ID, refreshToken, accessToken.Expiration, nil +} + +func (s *Storage) exchangeRefreshToken(ctx context.Context, request op.TokenExchangeRequest) (accessTokenID string, newRefreshToken string, expiration time.Time, err error) { + applicationID := request.GetClientID() + authTime := request.GetAuthTime() + + refreshTokenID := uuid.NewString() + accessToken, err := s.accessToken(applicationID, refreshTokenID, request.GetSubject(), request.GetAudience(), request.GetScopes()) + if err != nil { + return "", "", time.Time{}, err + } + + refreshToken, err := s.createRefreshToken(accessToken, nil, authTime) + if err != nil { + return "", "", time.Time{}, err + } + + return accessToken.ID, refreshToken, accessToken.Expiration, nil +} + +// TokenRequestByRefreshToken implements the op.Storage interface +// it will be called after parsing and validation of the refresh token request +func (s *Storage) TokenRequestByRefreshToken(ctx context.Context, refreshToken string) (op.RefreshTokenRequest, error) { + s.lock.Lock() + defer s.lock.Unlock() + token, ok := s.refreshTokens[refreshToken] + if !ok { + return nil, fmt.Errorf("invalid refresh_token") + } + return RefreshTokenRequestFromBusiness(token), nil +} + +// TerminateSession implements the op.Storage interface +// it will be called after the user signed out, therefore the access and refresh token of the user of this client must be removed +func (s *Storage) TerminateSession(ctx context.Context, userID string, clientID string) error { + s.lock.Lock() + defer s.lock.Unlock() + for _, token := range s.tokens { + if token.ApplicationID == clientID && token.Subject == userID { + delete(s.tokens, token.ID) + delete(s.refreshTokens, token.RefreshTokenID) + } + } + return nil +} + +// GetRefreshTokenInfo looks up a refresh token and returns the token id and user id. +// If given something that is not a refresh token, it must return error. +func (s *Storage) GetRefreshTokenInfo(ctx context.Context, clientID string, token string) (userID string, tokenID string, err error) { + refreshToken, ok := s.refreshTokens[token] + if !ok { + return "", "", op.ErrInvalidRefreshToken + } + return refreshToken.UserID, refreshToken.ID, nil +} + +// RevokeToken implements the op.Storage interface +// it will be called after parsing and validation of the token revocation request +func (s *Storage) RevokeToken(ctx context.Context, tokenIDOrToken string, userID string, clientID string) *oidc.Error { + // a single token was requested to be removed + s.lock.Lock() + defer s.lock.Unlock() + accessToken, ok := s.tokens[tokenIDOrToken] // tokenID + if ok { + if accessToken.ApplicationID != clientID { + return oidc.ErrInvalidClient().WithDescription("token was not issued for this client") + } + // if it is an access token, just remove it + // you could also remove the corresponding refresh token if really necessary + delete(s.tokens, accessToken.ID) + return nil + } + refreshToken, ok := s.refreshTokens[tokenIDOrToken] // token + if !ok { + // if the token is neither an access nor a refresh token, just ignore it, the expected behaviour of + // being not valid (anymore) is achieved + return nil + } + if refreshToken.ApplicationID != clientID { + return oidc.ErrInvalidClient().WithDescription("token was not issued for this client") + } + // if it is a refresh token, you will have to remove the access token as well + delete(s.refreshTokens, refreshToken.ID) + for _, accessToken := range s.tokens { + if accessToken.RefreshTokenID == refreshToken.ID { + delete(s.tokens, accessToken.ID) + return nil + } + } + return nil +} + +// SigningKey implements the op.Storage interface +// it will be called when creating the OpenID Provider +func (s *Storage) SigningKey(ctx context.Context) (op.SigningKey, error) { + // in this example the signing key is a static rsa.PrivateKey and the algorithm used is RS256 + // you would obviously have a more complex implementation and store / retrieve the key from your database as well + return &s.signingKey, nil +} + +// SignatureAlgorithms implements the op.Storage interface +// it will be called to get the sign +func (s *Storage) SignatureAlgorithms(context.Context) ([]jose.SignatureAlgorithm, error) { + return []jose.SignatureAlgorithm{s.signingKey.algorithm}, nil +} + +// KeySet implements the op.Storage interface +// it will be called to get the current (public) keys, among others for the keys_endpoint or for validating access_tokens on the userinfo_endpoint, ... +func (s *Storage) KeySet(ctx context.Context) ([]op.Key, error) { + // as mentioned above, this example only has a single signing key without key rotation, + // so it will directly use its public key + // + // when using key rotation you typically would store the public keys alongside the private keys in your database + // and give both of them an expiration date, with the public key having a longer lifetime + return []op.Key{&publicKey{s.signingKey}}, nil +} + +// GetClientByClientID implements the op.Storage interface +// it will be called whenever information (type, redirect_uris, ...) about the client behind the client_id is needed +func (s *Storage) GetClientByClientID(ctx context.Context, clientID string) (op.Client, error) { + s.lock.Lock() + defer s.lock.Unlock() + client, ok := s.clients[clientID] + if !ok { + return nil, fmt.Errorf("client not found") + } + return RedirectGlobsClient(client), nil +} + +// AuthorizeClientIDSecret implements the op.Storage interface +// it will be called for validating the client_id, client_secret on token or introspection requests +func (s *Storage) AuthorizeClientIDSecret(ctx context.Context, clientID, clientSecret string) error { + s.lock.Lock() + defer s.lock.Unlock() + client, ok := s.clients[clientID] + if !ok { + return fmt.Errorf("client not found") + } + // for this example we directly check the secret + // obviously you would not have the secret in plain text, but rather hashed and salted (e.g. using bcrypt) + if client.secret != clientSecret { + return fmt.Errorf("invalid secret") + } + return nil +} + +// SetUserinfoFromScopes implements the op.Storage interface. +// Provide an empty implementation and use SetUserinfoFromRequest instead. +func (s *Storage) SetUserinfoFromScopes(ctx context.Context, userinfo *oidc.UserInfo, userID, clientID string, scopes []string) error { + return nil +} + +// SetUserinfoFromRequests implements the op.CanSetUserinfoFromRequest interface. In the +// next major release, it will be required for op.Storage. +// It will be called for the creation of an id_token, so we'll just pass it to the private function without any further check +func (s *Storage) SetUserinfoFromRequest(ctx context.Context, userinfo *oidc.UserInfo, token op.IDTokenRequest, scopes []string) error { + return s.setUserinfo(ctx, userinfo, token.GetSubject(), token.GetClientID(), scopes) +} + +// SetUserinfoFromToken implements the op.Storage interface +// it will be called for the userinfo endpoint, so we read the token and pass the information from that to the private function +func (s *Storage) SetUserinfoFromToken(ctx context.Context, userinfo *oidc.UserInfo, tokenID, subject, origin string) error { + token, ok := func() (*Token, bool) { + s.lock.Lock() + defer s.lock.Unlock() + token, ok := s.tokens[tokenID] + return token, ok + }() + if !ok { + return fmt.Errorf("token is invalid or has expired") + } + // the userinfo endpoint should support CORS. If it's not possible to specify a specific origin in the CORS handler, + // and you have to specify a wildcard (*) origin, then you could also check here if the origin which called the userinfo endpoint here directly + // note that the origin can be empty (if called by a web client) + // + // if origin != "" { + // client, ok := s.clients[token.ApplicationID] + // if !ok { + // return fmt.Errorf("client not found") + // } + // if err := checkAllowedOrigins(client.allowedOrigins, origin); err != nil { + // return err + // } + //} + return s.setUserinfo(ctx, userinfo, token.Subject, token.ApplicationID, token.Scopes) +} + +// SetIntrospectionFromToken implements the op.Storage interface +// it will be called for the introspection endpoint, so we read the token and pass the information from that to the private function +func (s *Storage) SetIntrospectionFromToken(ctx context.Context, introspection *oidc.IntrospectionResponse, tokenID, subject, clientID string) error { + token, ok := func() (*Token, bool) { + s.lock.Lock() + defer s.lock.Unlock() + token, ok := s.tokens[tokenID] + return token, ok + }() + if !ok { + return fmt.Errorf("token is invalid or has expired") + } + // check if the client is part of the requested audience + for _, aud := range token.Audience { + if aud == clientID { + // the introspection response only has to return a boolean (active) if the token is active + // this will automatically be done by the library if you don't return an error + // you can also return further information about the user / associated token + // e.g. the userinfo (equivalent to userinfo endpoint) + + userInfo := new(oidc.UserInfo) + err := s.setUserinfo(ctx, userInfo, subject, clientID, token.Scopes) + if err != nil { + return err + } + introspection.SetUserInfo(userInfo) + //...and also the requested scopes... + introspection.Scope = token.Scopes + //...and the client the token was issued to + introspection.ClientID = token.ApplicationID + return nil + } + } + return fmt.Errorf("token is not valid for this client") +} + +// GetPrivateClaimsFromScopes implements the op.Storage interface +// it will be called for the creation of a JWT access token to assert claims for custom scopes +func (s *Storage) GetPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (claims map[string]interface{}, err error) { + return s.getPrivateClaimsFromScopes(ctx, userID, clientID, scopes) +} + +func (s *Storage) getPrivateClaimsFromScopes(ctx context.Context, userID, clientID string, scopes []string) (claims map[string]interface{}, err error) { + for _, scope := range scopes { + switch scope { + case CustomScope: + claims = appendClaim(claims, CustomClaim, customClaim(clientID)) + } + } + return claims, nil +} + +// GetKeyByIDAndClientID implements the op.Storage interface +// it will be called to validate the signatures of a JWT (JWT Profile Grant and Authentication) +func (s *Storage) GetKeyByIDAndClientID(ctx context.Context, keyID, clientID string) (*jose.JSONWebKey, error) { + s.lock.Lock() + defer s.lock.Unlock() + service, ok := s.services[clientID] + if !ok { + return nil, fmt.Errorf("clientID not found") + } + key, ok := service.keys[keyID] + if !ok { + return nil, fmt.Errorf("key not found") + } + return &jose.JSONWebKey{ + KeyID: keyID, + Use: "sig", + Key: key, + }, nil +} + +// ValidateJWTProfileScopes implements the op.Storage interface +// it will be called to validate the scopes of a JWT Profile Authorization Grant request +func (s *Storage) ValidateJWTProfileScopes(ctx context.Context, userID string, scopes []string) ([]string, error) { + allowedScopes := make([]string, 0) + for _, scope := range scopes { + if scope == oidc.ScopeOpenID { + allowedScopes = append(allowedScopes, scope) + } + } + return allowedScopes, nil +} + +// Health implements the op.Storage interface +func (s *Storage) Health(ctx context.Context) error { + return nil +} + +// createRefreshToken will store a refresh_token in-memory based on the provided information +func (s *Storage) createRefreshToken(accessToken *Token, amr []string, authTime time.Time) (string, error) { + s.lock.Lock() + defer s.lock.Unlock() + token := &RefreshToken{ + ID: accessToken.RefreshTokenID, + Token: accessToken.RefreshTokenID, + AuthTime: authTime, + AMR: amr, + ApplicationID: accessToken.ApplicationID, + UserID: accessToken.Subject, + Audience: accessToken.Audience, + Expiration: time.Now().Add(5 * time.Hour), + Scopes: accessToken.Scopes, + } + s.refreshTokens[token.ID] = token + return token.Token, nil +} + +// renewRefreshToken checks the provided refresh_token and creates a new one based on the current +func (s *Storage) renewRefreshToken(currentRefreshToken string) (string, string, error) { + s.lock.Lock() + defer s.lock.Unlock() + refreshToken, ok := s.refreshTokens[currentRefreshToken] + if !ok { + return "", "", fmt.Errorf("invalid refresh token") + } + // deletes the refresh token and all access tokens which were issued based on this refresh token + delete(s.refreshTokens, currentRefreshToken) + for _, token := range s.tokens { + if token.RefreshTokenID == currentRefreshToken { + delete(s.tokens, token.ID) + break + } + } + // creates a new refresh token based on the current one + token := uuid.NewString() + refreshToken.Token = token + refreshToken.ID = token + s.refreshTokens[token] = refreshToken + return token, refreshToken.ID, nil +} + +// accessToken will store an access_token in-memory based on the provided information +func (s *Storage) accessToken(applicationID, refreshTokenID, subject string, audience, scopes []string) (*Token, error) { + s.lock.Lock() + defer s.lock.Unlock() + token := &Token{ + ID: uuid.NewString(), + ApplicationID: applicationID, + RefreshTokenID: refreshTokenID, + Subject: subject, + Audience: audience, + Expiration: time.Now().Add(5 * time.Minute), + Scopes: scopes, + } + s.tokens[token.ID] = token + return token, nil +} + +// setUserinfo sets the info based on the user, scopes and if necessary the clientID +func (s *Storage) setUserinfo(ctx context.Context, userInfo *oidc.UserInfo, userID, clientID string, scopes []string) (err error) { + s.lock.Lock() + defer s.lock.Unlock() + user := s.userStore.GetUserByID(userID) + if user == nil { + return fmt.Errorf("user not found") + } + for _, scope := range scopes { + switch scope { + case oidc.ScopeOpenID: + userInfo.Subject = user.ID + case oidc.ScopeEmail: + userInfo.Email = user.Email + userInfo.EmailVerified = oidc.Bool(user.EmailVerified) + case oidc.ScopeProfile: + userInfo.PreferredUsername = user.Username + userInfo.Name = user.FirstName + " " + user.LastName + userInfo.FamilyName = user.LastName + userInfo.GivenName = user.FirstName + userInfo.Locale = oidc.NewLocale(user.PreferredLanguage) + userInfo.Email = user.Email + case oidc.ScopePhone: + userInfo.PhoneNumber = user.Phone + userInfo.PhoneNumberVerified = user.PhoneVerified + case CustomScope: + // you can also have a custom scope and assert public or custom claims based on that + userInfo.AppendClaims(CustomClaim, customClaim(clientID)) + } + } + return nil +} + +// ValidateTokenExchangeRequest implements the op.TokenExchangeStorage interface +// it will be called to validate parsed Token Exchange Grant request +func (s *Storage) ValidateTokenExchangeRequest(ctx context.Context, request op.TokenExchangeRequest) error { + if request.GetRequestedTokenType() == "" { + request.SetRequestedTokenType(oidc.RefreshTokenType) + } + + // Just an example, some use cases might need this use case + if request.GetExchangeSubjectTokenType() == oidc.IDTokenType && request.GetRequestedTokenType() == oidc.RefreshTokenType { + return errors.New("exchanging id_token to refresh_token is not supported") + } + + // Check impersonation permissions + if request.GetExchangeActor() == "" && !s.userStore.GetUserByID(request.GetExchangeSubject()).IsAdmin { + return errors.New("user doesn't have impersonation permission") + } + + allowedScopes := make([]string, 0) + for _, scope := range request.GetScopes() { + if scope == oidc.ScopeAddress { + continue + } + + if strings.HasPrefix(scope, CustomScopeImpersonatePrefix) { + subject := strings.TrimPrefix(scope, CustomScopeImpersonatePrefix) + request.SetSubject(subject) + } + + allowedScopes = append(allowedScopes, scope) + } + + request.SetCurrentScopes(allowedScopes) + + return nil +} + +// ValidateTokenExchangeRequest implements the op.TokenExchangeStorage interface +// Common use case is to store request for audit purposes. For this example we skip the storing. +func (s *Storage) CreateTokenExchangeRequest(ctx context.Context, request op.TokenExchangeRequest) error { + return nil +} + +// GetPrivateClaimsFromScopesForTokenExchange implements the op.TokenExchangeStorage interface +// it will be called for the creation of an exchanged JWT access token to assert claims for custom scopes +// plus adding token exchange specific claims related to delegation or impersonation +func (s *Storage) GetPrivateClaimsFromTokenExchangeRequest(ctx context.Context, request op.TokenExchangeRequest) (claims map[string]interface{}, err error) { + claims, err = s.getPrivateClaimsFromScopes(ctx, "", request.GetClientID(), request.GetScopes()) + if err != nil { + return nil, err + } + + for k, v := range s.getTokenExchangeClaims(ctx, request) { + claims = appendClaim(claims, k, v) + } + + return claims, nil +} + +// SetUserinfoFromScopesForTokenExchange implements the op.TokenExchangeStorage interface +// it will be called for the creation of an id_token - we are using the same private function as for other flows, +// plus adding token exchange specific claims related to delegation or impersonation +func (s *Storage) SetUserinfoFromTokenExchangeRequest(ctx context.Context, userinfo *oidc.UserInfo, request op.TokenExchangeRequest) error { + err := s.setUserinfo(ctx, userinfo, request.GetSubject(), request.GetClientID(), request.GetScopes()) + if err != nil { + return err + } + + for k, v := range s.getTokenExchangeClaims(ctx, request) { + userinfo.AppendClaims(k, v) + } + + return nil +} + +func (s *Storage) getTokenExchangeClaims(ctx context.Context, request op.TokenExchangeRequest) (claims map[string]interface{}) { + for _, scope := range request.GetScopes() { + switch { + case strings.HasPrefix(scope, CustomScopeImpersonatePrefix) && request.GetExchangeActor() == "": + // Set actor subject claim for impersonation flow + claims = appendClaim(claims, "act", map[string]interface{}{ + "sub": request.GetExchangeSubject(), + }) + } + } + + // Set actor subject claim for delegation flow + // if request.GetExchangeActor() != "" { + // claims = appendClaim(claims, "act", map[string]interface{}{ + // "sub": request.GetExchangeActor(), + // }) + // } + + return claims +} + +// getInfoFromRequest returns the clientID, authTime and amr depending on the op.TokenRequest type / implementation +func getInfoFromRequest(req op.TokenRequest) (clientID string, authTime time.Time, amr []string) { + authReq, ok := req.(*AuthRequest) // Code Flow (with scope offline_access) + if ok { + return authReq.ApplicationID, authReq.authTime, authReq.GetAMR() + } + refreshReq, ok := req.(*RefreshTokenRequest) // Refresh Token Request + if ok { + return refreshReq.ApplicationID, refreshReq.AuthTime, refreshReq.AMR + } + return "", time.Time{}, nil +} + +// customClaim demonstrates how to return custom claims based on provided information +func customClaim(clientID string) map[string]interface{} { + return map[string]interface{}{ + "client": clientID, + "other": "stuff", + } +} + +func appendClaim(claims map[string]interface{}, claim string, value interface{}) map[string]interface{} { + if claims == nil { + claims = make(map[string]interface{}) + } + claims[claim] = value + return claims +} + +type deviceAuthorizationEntry struct { + deviceCode string + userCode string + state *op.DeviceAuthorizationState +} + +func (s *Storage) StoreDeviceAuthorization(ctx context.Context, clientID, deviceCode, userCode string, expires time.Time, scopes []string) error { + s.lock.Lock() + defer s.lock.Unlock() + + if _, ok := s.clients[clientID]; !ok { + return errors.New("client not found") + } + + if _, ok := s.userCodes[userCode]; ok { + return op.ErrDuplicateUserCode + } + + s.deviceCodes[deviceCode] = deviceAuthorizationEntry{ + deviceCode: deviceCode, + userCode: userCode, + state: &op.DeviceAuthorizationState{ + ClientID: clientID, + Scopes: scopes, + Expires: expires, + }, + } + + s.userCodes[userCode] = deviceCode + return nil +} + +func (s *Storage) GetDeviceAuthorizatonState(ctx context.Context, clientID, deviceCode string) (*op.DeviceAuthorizationState, error) { + if ctx.Err() != nil { + return nil, ctx.Err() + } + + s.lock.Lock() + defer s.lock.Unlock() + + entry, ok := s.deviceCodes[deviceCode] + if !ok || entry.state.ClientID != clientID { + return nil, errors.New("device code not found for client") // is there a standard not found error in the framework? + } + + return entry.state, nil +} + +func (s *Storage) GetDeviceAuthorizationByUserCode(ctx context.Context, userCode string) (*op.DeviceAuthorizationState, error) { + s.lock.Lock() + defer s.lock.Unlock() + + entry, ok := s.deviceCodes[s.userCodes[userCode]] + if !ok { + return nil, errors.New("user code not found") + } + + return entry.state, nil +} + +func (s *Storage) CompleteDeviceAuthorization(ctx context.Context, userCode, subject string) error { + s.lock.Lock() + defer s.lock.Unlock() + + entry, ok := s.deviceCodes[s.userCodes[userCode]] + if !ok { + return errors.New("user code not found") + } + + entry.state.Subject = subject + entry.state.Done = true + return nil +} + +func (s *Storage) DenyDeviceAuthorization(ctx context.Context, userCode string) error { + s.lock.Lock() + defer s.lock.Unlock() + + s.deviceCodes[s.userCodes[userCode]].state.Denied = true + return nil +} + +// AuthRequestDone is used by testing and is not required to implement op.Storage +func (s *Storage) AuthRequestDone(id string) error { + s.lock.Lock() + defer s.lock.Unlock() + + if req, ok := s.authRequests[id]; ok { + req.done = true + return nil + } + + return errors.New("request not found") +} + +func (s *Storage) ClientCredentials(ctx context.Context, clientID, clientSecret string) (op.Client, error) { + s.lock.Lock() + defer s.lock.Unlock() + + client, ok := s.serviceUsers[clientID] + if !ok { + return nil, errors.New("wrong service user or password") + } + if client.secret != clientSecret { + return nil, errors.New("wrong service user or password") + } + + return client, nil +} + +func (s *Storage) ClientCredentialsTokenRequest(ctx context.Context, clientID string, scopes []string) (op.TokenRequest, error) { + client, ok := s.serviceUsers[clientID] + if !ok { + return nil, errors.New("wrong service user or password") + } + + return &oidc.JWTTokenRequest{ + Subject: client.id, + Audience: []string{clientID}, + Scopes: scopes, + }, nil +} diff --git a/auth/storage/token.go b/auth/storage/token.go new file mode 100644 index 0000000..ad907e3 --- /dev/null +++ b/auth/storage/token.go @@ -0,0 +1,25 @@ +package storage + +import "time" + +type Token struct { + ID string + ApplicationID string + Subject string + RefreshTokenID string + Audience []string + Expiration time.Time + Scopes []string +} + +type RefreshToken struct { + ID string + Token string + AuthTime time.Time + AMR []string + Audience []string + UserID string + ApplicationID string + Expiration time.Time + Scopes []string +} diff --git a/auth/storage/user.go b/auth/storage/user.go new file mode 100644 index 0000000..aeff184 --- /dev/null +++ b/auth/storage/user.go @@ -0,0 +1,88 @@ +package storage + +import ( + "crypto/rsa" + "strings" + + "golang.org/x/text/language" +) + +type User struct { + ID string + Username string + Password string + FirstName string + LastName string + Email string + EmailVerified bool + Phone string + PhoneVerified bool + PreferredLanguage language.Tag + IsAdmin bool +} + +type Service struct { + keys map[string]*rsa.PublicKey +} + +type UserStore interface { + GetUserByID(string) *User + GetUserByUsername(string) *User + ExampleClientID() string +} + +type userStore struct { + users map[string]*User +} + +func NewUserStore(issuer string) UserStore { + hostname := strings.Split(strings.Split(issuer, "://")[1], ":")[0] + return userStore{ + users: map[string]*User{ + "id1": { + ID: "id1", + Username: "alice@" + hostname, + Password: "verysecure", + FirstName: "Alice", + LastName: "Kingsley", + Email: "alice@" + hostname, + EmailVerified: true, + Phone: "", + PhoneVerified: false, + PreferredLanguage: language.German, + IsAdmin: true, + }, + "id2": { + ID: "id2", + Username: "bob@" + hostname, + Password: "verysecure", + FirstName: "Bob", + LastName: "Smith", + Email: "bob@" + hostname, + EmailVerified: true, + Phone: "", + PhoneVerified: false, + PreferredLanguage: language.German, + IsAdmin: false, + }, + }, + } +} + +// ExampleClientID is only used in the example server +func (u userStore) ExampleClientID() string { + return "service" +} + +func (u userStore) GetUserByID(id string) *User { + return u.users[id] +} + +func (u userStore) GetUserByUsername(username string) *User { + for _, user := range u.users { + if user.Username == username { + return user + } + } + return nil +} diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..5746c4b --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,57 @@ +services: + auth: + build: + dockerfile: ./docker/auth.Dockerfile + ports: + - "9998:9998" + auth-ext: + build: + dockerfile: ./docker/auth-ext.Dockerfile + environment: + BACKEND_HOST: auth-ext + OIDC_CLIENT_ID: web + OIDC_CLIENT_SECRET: secret + OIDC_ISSUER: http://auth:9998/ + ports: + - "5000:5000" + frontend: + build: + dockerfile: ./docker/frontend.Dockerfile + environment: + OIDC_CLIENT_ID: web + OIDC_ISSUER: http://localhost:9998/ + OIDC_RESPONSE_TYPE: code + OIDC_SCOPE: openid profile + ports: + - "5173:80" + gallery: + build: + dockerfile: ./docker/gallery.Dockerfile + ports: + - "8081:8081" + + minio: + image: minio/minio:RELEASE.2023-10-14T05-17-22Z + command: server --address :9000 --console-address :9001 --compat /data + environment: + MINIO_ROOT_USER: minio + MINIO_ROOT_PASSWORD: mona_value_abc124 + ports: + - "9000:9000" + - "9001:9001" + volumes: + - minio_data:/data + storage: + build: + dockerfile: ./docker/storage.Dockerfile + depends_on: + minio: + condition: service_started + environment: + MINIO_URL: http://minio:9000 + MINIO_USERNAME: minio + MINIO_PASSWORD: mona_value_abc124 + ports: + - "8082:8082" +volumes: + minio_data: {} diff --git a/docker/auth-ext.Dockerfile b/docker/auth-ext.Dockerfile new file mode 100644 index 0000000..d47be50 --- /dev/null +++ b/docker/auth-ext.Dockerfile @@ -0,0 +1,19 @@ +# syntax=docker/dockerfile:1.4 +FROM python:3.10-bullseye + +WORKDIR /code + +COPY auth-ext /code + +RUN apt-get update && \ + apt-get install -y \ + libcairo2-dev \ + pkg-config \ + python3-dev \ + libgirepository1.0-dev + +RUN --mount=type=cache,target=/root/.cache/pip \ + pip3 install -r requirements.txt + +ENTRYPOINT ["python3"] +CMD ["app.py"] diff --git a/docker/auth.Dockerfile b/docker/auth.Dockerfile new file mode 100644 index 0000000..b4df642 --- /dev/null +++ b/docker/auth.Dockerfile @@ -0,0 +1,10 @@ +FROM golang:1.20 + +WORKDIR /code + +COPY auth /code +RUN go mod download + +RUN go build -o server + +CMD ["./server"] \ No newline at end of file diff --git a/docker/frontend.Dockerfile b/docker/frontend.Dockerfile new file mode 100644 index 0000000..af1dad8 --- /dev/null +++ b/docker/frontend.Dockerfile @@ -0,0 +1,11 @@ +FROM docker.io/node:18.16.0-buster + +RUN npm install -g @vue/cli + +WORKDIR /code +COPY frontend /code + +RUN npm install + +ENTRYPOINT ["npm"] +CMD ["run", "serve"] diff --git a/docker/gallery.Dockerfile b/docker/gallery.Dockerfile new file mode 100644 index 0000000..2d5c23e --- /dev/null +++ b/docker/gallery.Dockerfile @@ -0,0 +1,10 @@ +FROM golang:1.15 + +WORKDIR /code + +COPY gallery /code +RUN go mod download + +RUN go build -o gallery + +CMD ["./gallery", "config.json"] \ No newline at end of file diff --git a/docker/storage.Dockerfile b/docker/storage.Dockerfile new file mode 100644 index 0000000..f2b187d --- /dev/null +++ b/docker/storage.Dockerfile @@ -0,0 +1,25 @@ +# +=====================================git ========================= +# | compile storage +# +============================================================== + +FROM azul/zulu-openjdk:21 as build +WORKDIR /code + +COPY storage . + +RUN apt-get update \ + && apt-get install -y maven \ + && mvn -Dmaven.test.skip=true clean package + +# +============================================================== +# | package storage +# +============================================================== + +FROM azul/zulu-openjdk:21-jre +WORKDIR /app + +COPY --from=build \ + /code/target/storage.jar \ + . + +ENTRYPOINT ["java", "-jar", "storage.jar"] \ No newline at end of file diff --git a/exercise.md b/exercise.md new file mode 100644 index 0000000..7c7574c --- /dev/null +++ b/exercise.md @@ -0,0 +1,312 @@ +# Universe 2023 - Workshop Exercises + +Welcome to the workshop **Harnessing AI: Next Level Strategies for Advanced Security**! +Below you will find the exercises to complete as a part of this workshop. There will be a set amount of time to complete each one. +We will provide a walkthrough of the solution after each exercise. +If you get stuck there are hints along the way. Additionaly you can access GIF-based solutions for each exercise. + + +## Lab 1 - Secret Scanning AI + +If you are interested in participating in the **limited public beta** for secret scanning please join the [waitlist](https://github.com/features/preview/security) and the GitHub staff will be in touch. You can find the terms and conditions [here](https://docs.github.com/en/site-policy/github-terms/github-terms-for-additional-products-and-features). + +### Exercise 1 - AI-generated custom patterns - 5 mins + +This Universe, we are excited to be launching the **AI-generated custom patterns** feature as a limited public beta. +In this exercise, we will generate a custom pattern using this new feature. +
+Prerequiste - secret scanning must be enabled for this exercise. This feature should already be enabled by default for this repository. If not, please follow the instructions here to enable secret scanning. + +1. Navigate to the `Settings` tab of the repository, click on the `Code security and analysis` section, and click `Enable` under secret scanning. + + ![secret-scanning-enablement](https://github.com/octodemo/universe-wip/assets/68650974/fd1e12ad-5f36-4a77-a3a5-6e97db61c6ad) + +
+ +**Scenario** + +You are a part of the security team, and it has been brought to your attention that the MinIO password `mona_value_abc124` for the object store has been leaked in the code! +You want to write a custom pattern to detect this. + +1. Navigate to `Settings` tab of the repository, click on `Code security and analysis` section, and under `Custom Patterns` click on `New pattern` +2. In the top right hand corner, click on `Generate with AI` +
+ Solution + + ![secret-scanning-custom-ai-setting](https://github.com/octodemo/universe-wip/assets/68650974/ae7156c6-268e-4b56-8cb5-ad30c5fd1a2f) + +
+ +3. Fill in the options `I want a regular expression that` and `Examples of what I am looking for` + +
+ Hint + You want to find a string that starts with `mona_value_` +
+ +
+ Solution + + ![secret-scanning-mona-value](https://github.com/octodemo/universe-wip/assets/68650974/052be656-400a-4f25-a0be-9d9f90602f93) + +
+ +4. Assume that we know that the custom secret pattern always ends with an alphanumeric character of length 6. Can you make the regex pattern more precise? For the most accurate results, it is imperative when dealing with AI that we provide as much detail as possible with as much context as possible. + +
+ Hint + Provide the AI with more details about the pattern. Consider factors such as length. What about character composition? Is it entirely numerical? +
+ +
+ Solution + + ![mona-value-6](https://github.com/octodemo/universe-wip/assets/68650974/e7eca0d5-98fb-4425-9da8-e1ae8a282df1) + +
+ +### Exercise 2 - generic secret detection using AI - 5 mins + +At Universe 2023 we will also be shipping the **generic secret detection using AI** feature as a limited public beta. Enabling this feature will use an AI model to detect additional secrets beyond the secrets detected with regular expression. + +1. Enable this feature by navigating to the `Settings` tab of the repository, click on the `Code security and analysis` section, and tick the checkbox `Use AI detection to find additional secrets` under secret scanning. + +
+ Solution + + ![generic-ai-secret](https://github.com/octodemo/universe-wip/assets/68650974/0519aa0d-75a8-45e3-9fc5-f8d34ffd9bb2) + +
+ +2. Add Bob's credentials to the end of the `password.txt` file located at the root of the repository. + + ```txt + username=bob@localhost + password=verysecure1234 + ``` + +
+ Solution + + ![exercise-1-lab2-add-password](https://github.com/githubuniverseworkshops/mona-gallery-s-samadi-demo/assets/68650974/db6949bc-9608-41a1-8547-5f496c8763ef) + + +
+ +3. To view detected alerts, navigate to the `Security` tab, under `Secret Scanning`, click the `Other` option + +
+ Solution + + ![exercise-1-lab2-view](https://github.com/githubuniverseworkshops/mona-gallery-s-samadi-demo/assets/68650974/5eb458df-ab9d-4e00-95ed-d5381760a5fa) + + +
+ +## Lab 2 - Code Scanning AI + + +**Scenario** + +You have just joined a new team as a developer. To familiarize yourself with the codebase, you've been tasked with remediating some code scanning vulnerabilities in the repository. + +You will be remediating an existing SQL Injection vulnerability in `main.go` on **line 309**. +There are two tasks to remediate this vulnerability: +1. sanitize input in the javascript (Exercise 1) +2. fix the SQL prepare statement in the go code (Exercise 2 and Exercise 3) + +### Exercise 1 - AI generated autofix on javascript pull requests - 10 mins + +This Universe we will be launching the **code scanning autofix** feature as a limited public beta. This feature uses AI to generate fixes on pull requests that contain JavaScript CodeQL alerts. We will see this feature in action in this exercise. If you are interested in participating in this limited public beta, please reach out to your Account Manager if you are an existing customer. Otherwise, reach out to the GitHub Sales Team for assistance. You can find the terms and conditions [here](https://docs.github.com/en/site-policy/github-terms/github-terms-for-additional-products-and-features). + +Input sanitization is a fundamental security practice to prevent SQL injection attacks. Let's add a sanitize method in the javascript to help in mitigating against injection attack vectors. + +You can solve this exercise using either the codespaces or the UI. Codespaces is preferable as this is what a developer would use under normal circumstances. However, if codespaces is not loading for you please use the UI. + + +1. Create a branch called `sql-injection-fix` and push it to the remote repo. + +
+ Hint + Run the command: + + ``` + $ git checkout -b sql-injection-fix + $ git push -u origin sql-injection-fix + ``` +
+ +
+ Solution + + ![create-branch](https://github.com/octodemo/universe-wip/assets/68650974/4a162cb4-62d7-4ce6-9114-c5efefe60b2d) + +
+ + +2. Add the following sanitization function on **line 233** of `frontend/components/Gallery.vue` + + ```js + function sanitizeInput(input) { + if (input == null) { + return ""; + } + //escape all occurances of apostrophe + input = input.replace("'", "''"); + + return input; + } + ``` + +3. Call the sanization function from the Update function by placing the following call on **line 347** of `/frontend/components/Gallery.vue` + + ```js + artItem.description = sanitizeInput(artItem.description) + artItem.title = sanitizeInput(artItem.title) + ``` + +4. Commit and push the code + +
+ Solution for steps 2-4 + + ![sanitization](https://github.com/octodemo/universe-wip/assets/68650974/a85a6690-607d-467e-b6bf-3566ad73d5b9) + +
+ +5. Raise a pull request to the `main` branch. Wait for the scans to complete and you should see a CodeQL javascript alert in your pull request. Oh no! There is a vulnerability in our vulnerability! Lucky we have autofix. + +**Autofix feature** + +The autofix feature suggests fixes for CodeQL alerts raised as a part of the pull request. At the moment, it only supports JavaScript. Our sanitize function only replaced the first occurance of the string. Autofix has suggested a fix to replace the string with a regular expression and uses the g flag to ensure all occurrences are replaced. You should be able to see an autofix suggestion as a part of the pull request. + +![autofix](https://github.com/octodemo/universe-wip/assets/68650974/5a8e2c68-fc68-47b1-ae6d-c0814444530c) + + +### Exercise 2 - learning with Copilot - 5 mins + +1. Navigate to `Copilot Chat` icon in your Visual Studio Code IDE (Codespace) +2. Open the `main.go` file under `gallery` folder. Navigate to **line 309** which represents an injection vulnerability and ask GitHub Copilot Chat to explain the vulnerability +
+ Solution + + ![learn-app-sec](https://github.com/octodemo/universe-wip/assets/79184790/e50d3566-5829-41eb-8782-f226bbfed061) + +
+ + +### Exercise 3 - remediating vulnerabilities with Copilot - 10 mins + +Our sanitization function is limited to the user interface (UI). If we expose the Update method through an API or another medium, we remain susceptible to vulnerabilities. Let's use Copilot to remediate the vulnerability. + +1. In Codespaces, use the Sarif Viewer to navigate to the SQL Injection vulnerability located in `gallery/main.go` on **line 309**. Note if the Sarif Viewer is not loading the correct SARIF you can use the one provided in the `universe-utils/go.sarif` + +2. Hover over the alert and select `Fix using Copilot` +3. Copilot will propose a fix. Review proposed fix and click `Accept` +4. Commit, push the fix, and merge in the PR to resolve the alert + +
+ Solution + +![copilot-fix](https://github.com/octodemo/universe-wip/assets/68650974/8d8e7e54-ba87-444a-92fb-17f22f7a730a) + +
+ +### Bonus XSS Exercise: Prompting Copilot Chat to learn about custom CodeQL query - 5 mins + +**Prompt Engineering** involves formulating a prompt to optimize the model's ability to generate the most valuable prediction for the user. + +A prompt commonly involves at least one of the following elements: + +- Instruction - You can use natural language to provide instructions. However, Github Copilot Chat also offers pre-configured slash command choices as default templates to help steer the Language Model (LLM) more effectively toward your intended goal. +- Context - GitHub Copilot Chat lives in the context of an IDE such as Visual Studio Code (VS Code). This involves having relevant files open in each tab and highlighting the code snippet your interested in +- Examples - Provide examples of the desired output. We saw an example of this when generating regexes for secret scanning. Prompting falls into two broad categories: + - Zero shot prompting: this is when no examples are provided to the model. + - Few-shot prompting: this is when examples are provided to the model , it serves as conditioning for subsequent examples where we would like the model to generate a response. +- Output - Specify the desired output format. Some examples within Copilot instruction include: + - `/explain` - will output text file + - `Generate Docs` - will create comments in the code + - `/fix` - will output code + +**Scenario** + +There is a custom codeql query written specifically for finding vue related xss vulnerabilities specific to this codebase. This query can be found in the `queries` folder. Use **Copilot Chat** to better understand this query. + + +Initially, we want to see what happens when we don’t provide any context to Copilot Chat. +1. Close all tabs and open Copilot chat +2. Type in the following into the chat `/explain vue-xss.ql` + +Was this the output you expected? A key factor contributing to occasional inaccuracies in LLM outputs is the absence of context. These models heavily depend on the information provided in the context. When the context is ambiguous or lacks detail, the model might infer assumptions that result in inaccurate responses. + +
+ Solution + +![copilot-ex-1](https://github.com/githubuniverseworkshops/GHAS-AI-Template/assets/68650974/e3bc3e65-e71e-4e92-aefe-6dd6e0a760b4) + +
+ +Now let's add in some context. Recall that the context for GitHub Copilot is the IDE. + +3. Open the `vue-xss.ql` query in a tab and run the prompt from the previous exercise: + `/explain vue-xss.ql` + +Was the output different? + +
+ Solution + + ![copilot-ex-2](https://github.com/githubuniverseworkshops/GHAS-AI-Template/assets/68650974/92a79ff0-7a7e-493c-bd17-d5137d0b7c46) + +
+ + +The more specific and concise you can be about your problem statement the more accurate the results that will be generated. For example, let’s say that we want to understand the 2 predicates and their regexes between **line 42 and line 46**. + +4. Try highlighting between these lines and ask copilot for an explanation. + +
+ Solution + + ![copilot-ex-3](https://github.com/githubuniverseworkshops/GHAS-AI-Template/assets/68650974/e50d2e11-f274-442a-91bf-15b9e75919e8) + +
+ +Let's say you wanted to comment the CodeQL query so that it is easier to understand for new joiners of the repository. + +5. Highlight the predicate `resolveRefForArraySourceValue` from **line 60 to line 100** and use the option `Generate Docs` + +
+ Solution + +![copilot-ex-4](https://github.com/githubuniverseworkshops/GHAS-AI-Template/assets/68650974/03850e4c-fc37-4209-98d4-9ad58dc103b9) + + +
+ + +## Lab 3 - Threat Modelling with GitHub Copilot + +### Exercise 1 - threat modelling with GitHub Copilot - 5 mins + +**Scenario** + +Threat modelling is often a manual and specialised task conducted by security teams. We can automate some of this process using Copilot. + +In this **demo** we will be using the GitHub Copilot Chat feature from a security practitioner's perspective. We'll observe how leveraging an AI-assisted tool can begin to gain additional context regarding the application's threat boundaries. + +1. Let's Ask GitHub Copilot Chat to explain to us the DB interactions in the `gallery` module of the mona gallery application +
+ Solution + + ![threat-modelling-1](https://github.com/octodemo/universe-wip/assets/79184790/104c8234-4eb0-40d2-8ce4-949ac8cc6571) +
+ + +2. In the `storage` module lets ask GitHub Copilot Chat to explain to us the threat landscape of the `BlobController.java` code +
+ Solution + + ![threat-model-2](https://github.com/octodemo/universe-wip/assets/79184790/ce465948-c039-482f-bba4-1890230d48ce) + +
diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..38adffa --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,28 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +.DS_Store +dist +dist-ssr +coverage +*.local + +/cypress/videos/ +/cypress/screenshots/ + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..8d6c5ef --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,29 @@ +# frontend + +This template should help get you started developing with Vue 3 in Vite. + +## Recommended IDE Setup + +[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin). + +## Customize configuration + +See [Vite Configuration Reference](https://vitejs.dev/config/). + +## Project Setup + +```sh +npm install +``` + +### Compile and Hot-Reload for Development + +```sh +npm run dev +``` + +### Compile and Minify for Production + +```sh +npm run build +``` diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..de495f8 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,16 @@ + + + + + + + + Vite App + + + +
+ + + + \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..744a9f1 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,917 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "@popperjs/core": "^2.11.8", + "axios": "^1.5.1", + "bootstrap": "^5.3.1", + "pinia": "^2.1.6", + "vue": "^3.3.4", + "vue-router": "^4.2.4", + "vuex": "^4.0.2" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^4.3.1", + "vite": "^4.4.9" + } + }, + "node_modules/@babel/parser": { + "version": "7.22.14", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.14.tgz", + "integrity": "sha512-1KucTHgOvaw/LzCVrEOAyXkr9rQlp0A1HiHRYnSUE9dmb8PvPW7o5sscg+5169r54n3vGlbx6GevTE/Iw/P3AQ==", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.3.4.tgz", + "integrity": "sha512-ciXNIHKPriERBisHFBvnTbfKa6r9SAesOYXeGDzgegcvy9Q4xdScSHAmKbNT0M3O0S9LKhIf5/G+UYG4NnnzYw==", + "dev": true, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.4.tgz", + "integrity": "sha512-cquyDNvZ6jTbf/+x+AgM2Arrp6G4Dzbb0R64jiG804HRMfRiFXWI6kqUVqZ6ZR0bQhIoQjB4+2bhNtVwndW15g==", + "dependencies": { + "@babel/parser": "^7.21.3", + "@vue/shared": "3.3.4", + "estree-walker": "^2.0.2", + "source-map-js": "^1.0.2" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.3.4.tgz", + "integrity": "sha512-wyM+OjOVpuUukIq6p5+nwHYtj9cFroz9cwkfmP9O1nzH68BenTTv0u7/ndggT8cIQlnBeOo6sUT/gvHcIkLA5w==", + "dependencies": { + "@vue/compiler-core": "3.3.4", + "@vue/shared": "3.3.4" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.3.4.tgz", + "integrity": "sha512-6y/d8uw+5TkCuzBkgLS0v3lSM3hJDntFEiUORM11pQ/hKvkhSKZrXW6i69UyXlJQisJxuUEJKAWEqWbWsLeNKQ==", + "dependencies": { + "@babel/parser": "^7.20.15", + "@vue/compiler-core": "3.3.4", + "@vue/compiler-dom": "3.3.4", + "@vue/compiler-ssr": "3.3.4", + "@vue/reactivity-transform": "3.3.4", + "@vue/shared": "3.3.4", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.0", + "postcss": "^8.1.10", + "source-map-js": "^1.0.2" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.3.4.tgz", + "integrity": "sha512-m0v6oKpup2nMSehwA6Uuu+j+wEwcy7QmwMkVNVfrV9P2qE5KshC6RwOCq8fjGS/Eak/uNb8AaWekfiXxbBB6gQ==", + "dependencies": { + "@vue/compiler-dom": "3.3.4", + "@vue/shared": "3.3.4" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.5.0.tgz", + "integrity": "sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q==" + }, + "node_modules/@vue/reactivity": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.3.4.tgz", + "integrity": "sha512-kLTDLwd0B1jG08NBF3R5rqULtv/f8x3rOFByTDz4J53ttIQEDmALqKqXY0J+XQeN0aV2FBxY8nJDf88yvOPAqQ==", + "dependencies": { + "@vue/shared": "3.3.4" + } + }, + "node_modules/@vue/reactivity-transform": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.3.4.tgz", + "integrity": "sha512-MXgwjako4nu5WFLAjpBnCj/ieqcjE2aJBINUNQzkZQfzIZA4xn+0fV1tIYBJvvva3N3OvKGofRLvQIwEQPpaXw==", + "dependencies": { + "@babel/parser": "^7.20.15", + "@vue/compiler-core": "3.3.4", + "@vue/shared": "3.3.4", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.0" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.3.4.tgz", + "integrity": "sha512-R+bqxMN6pWO7zGI4OMlmvePOdP2c93GsHFM/siJI7O2nxFRzj55pLwkpCedEY+bTMgp5miZ8CxfIZo3S+gFqvA==", + "dependencies": { + "@vue/reactivity": "3.3.4", + "@vue/shared": "3.3.4" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.3.4.tgz", + "integrity": "sha512-Aj5bTJ3u5sFsUckRghsNjVTtxZQ1OyMWCr5dZRAPijF/0Vy4xEoRCwLyHXcj4D0UFbJ4lbx3gPTgg06K/GnPnQ==", + "dependencies": { + "@vue/runtime-core": "3.3.4", + "@vue/shared": "3.3.4", + "csstype": "^3.1.1" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.3.4.tgz", + "integrity": "sha512-Q6jDDzR23ViIb67v+vM1Dqntu+HUexQcsWKhhQa4ARVzxOY2HbC7QRW/ggkDBd5BU+uM1sV6XOAP0b216o34JQ==", + "dependencies": { + "@vue/compiler-ssr": "3.3.4", + "@vue/shared": "3.3.4" + }, + "peerDependencies": { + "vue": "3.3.4" + } + }, + "node_modules/@vue/shared": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.4.tgz", + "integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.1.tgz", + "integrity": "sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/bootstrap": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.1.tgz", + "integrity": "sha512-jzwza3Yagduci2x0rr9MeFSORjcHpt0lRZukZPZQJT1Dth5qzV7XcgGqYzi39KGAVYR8QEDVoO0ubFKOxzMG+g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "peerDependencies": { + "@popperjs/core": "^2.11.8" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, + "node_modules/follow-redirects": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", + "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.3", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.3.tgz", + "integrity": "sha512-B7xGbll2fG/VjP+SWg4sX3JynwIU0mjoTc6MPpKNuIvftk6u6vqhDnk1R80b8C2GBR6ywqy+1DcKBrevBg+bmw==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "node_modules/pinia": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.1.6.tgz", + "integrity": "sha512-bIU6QuE5qZviMmct5XwCesXelb5VavdOWKWaB17ggk++NUwQWWbP5YnsONTk3b752QkW9sACiR81rorpeOMSvQ==", + "dependencies": { + "@vue/devtools-api": "^6.5.0", + "vue-demi": ">=0.14.5" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "@vue/composition-api": "^1.4.0", + "typescript": ">=4.4.4", + "vue": "^2.6.14 || ^3.3.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/pinia/node_modules/vue-demi": { + "version": "0.14.6", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.6.tgz", + "integrity": "sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==", + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/postcss": { + "version": "8.4.29", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.29.tgz", + "integrity": "sha512-cbI+jaqIeu/VGqXEarWkRCCffhjgXc0qjBtXpqJhTBohMUjUQnbBr0xqX3vEKudc4iviTewcJo5ajcec5+wdJw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/rollup": { + "version": "3.28.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.28.1.tgz", + "integrity": "sha512-R9OMQmIHJm9znrU3m3cpE8uhN0fGdXiawME7aZIpQqvpS/85+Vt1Hq1/yVIcYfOmaQiHjvXkQAoJukvLpau6Yw==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/vite": { + "version": "4.4.9", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.9.tgz", + "integrity": "sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==", + "dev": true, + "dependencies": { + "esbuild": "^0.18.10", + "postcss": "^8.4.27", + "rollup": "^3.27.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.3.4.tgz", + "integrity": "sha512-VTyEYn3yvIeY1Py0WaYGZsXnz3y5UnGi62GjVEqvEGPl6nxbOrCXbVOTQWBEJUqAyTUk2uJ5JLVnYJ6ZzGbrSw==", + "dependencies": { + "@vue/compiler-dom": "3.3.4", + "@vue/compiler-sfc": "3.3.4", + "@vue/runtime-dom": "3.3.4", + "@vue/server-renderer": "3.3.4", + "@vue/shared": "3.3.4" + } + }, + "node_modules/vue-router": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.2.4.tgz", + "integrity": "sha512-9PISkmaCO02OzPVOMq2w82ilty6+xJmQrarYZDkjZBfl4RvYAlt4PKnEX21oW4KTtWfa9OuO/b3qk1Od3AEdCQ==", + "dependencies": { + "@vue/devtools-api": "^6.5.0" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/vuex": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vuex/-/vuex-4.0.2.tgz", + "integrity": "sha512-M6r8uxELjZIK8kTKDGgZTYX/ahzblnzC4isU1tpmEuOIIKmV+TRdc+H4s8ds2NuZ7wpUTdGRzJRtoj+lI+pc0Q==", + "dependencies": { + "@vue/devtools-api": "^6.0.0-beta.11" + }, + "peerDependencies": { + "vue": "^3.0.2" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..3cb094f --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,24 @@ +{ + "name": "frontend", + "version": "0.0.0", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "serve": "vite --host=0.0.0.0 --port=80" + }, + "dependencies": { + "@popperjs/core": "^2.11.8", + "axios": "^1.5.1", + "bootstrap": "^5.3.1", + "pinia": "^2.1.6", + "vue": "^3.3.4", + "vue-router": "^4.2.4", + "vuex": "^4.0.2" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^4.3.1", + "vite": "^4.4.9" + } +} \ No newline at end of file diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico new file mode 100644 index 0000000..df36fcf Binary files /dev/null and b/frontend/public/favicon.ico differ diff --git a/frontend/public/img/gh-mark.png b/frontend/public/img/gh-mark.png new file mode 100644 index 0000000..081af6c Binary files /dev/null and b/frontend/public/img/gh-mark.png differ diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..b8232f6 --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,47 @@ + + + + + + + \ No newline at end of file diff --git a/frontend/src/assets/base.css b/frontend/src/assets/base.css new file mode 100644 index 0000000..583a947 --- /dev/null +++ b/frontend/src/assets/base.css @@ -0,0 +1,72 @@ +:root { + --vt-c-white: #ffffff; + --vt-c-white-soft: #f8f8f8; + --vt-c-white-mute: #f2f2f2; + + --vt-c-black: #181818; + --vt-c-black-soft: #222222; + --vt-c-black-mute: #282828; + + --vt-c-indigo: #2c3e50; + + --vt-c-divider-light-1: rgba(60, 60, 60, 0.29); + --vt-c-divider-light-2: rgba(60, 60, 60, 0.12); + --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65); + --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48); + + --vt-c-text-light-1: var(--vt-c-indigo); + --vt-c-text-light-2: rgba(60, 60, 60, 0.66); + --vt-c-text-dark-1: var(--vt-c-white); + --vt-c-text-dark-2: rgba(235, 235, 235, 0.64); +} + + +:root { + --color-background: var(--vt-c-white); + --color-background-soft: var(--vt-c-white-soft); + --color-background-mute: var(--vt-c-white-mute); + + --color-border: var(--vt-c-divider-light-2); + --color-border-hover: var(--vt-c-divider-light-1); + + --color-heading: var(--vt-c-text-light-1); + --color-text: var(--vt-c-text-light-1); + + --section-gap: 160px; +} + +@media (prefers-color-scheme: dark) { + :root { + --color-background: var(--vt-c-black); + --color-background-soft: var(--vt-c-black-soft); + --color-background-mute: var(--vt-c-black-mute); + + --color-border: var(--vt-c-divider-dark-2); + --color-border-hover: var(--vt-c-divider-dark-1); + + --color-heading: var(--vt-c-text-dark-1); + --color-text: var(--vt-c-text-dark-2); + } +} + +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + font-weight: normal; +} + +body { + min-height: 100vh; + color: var(--color-text); + background: var(--color-background); + transition: color 0.5s, background-color 0.5s; + line-height: 1.6; + font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, + Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; + font-size: 15px; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} \ No newline at end of file diff --git a/frontend/src/assets/images/NUX_Octodex.gif b/frontend/src/assets/images/NUX_Octodex.gif new file mode 100644 index 0000000..4f69e56 Binary files /dev/null and b/frontend/src/assets/images/NUX_Octodex.gif differ diff --git a/frontend/src/assets/images/background.jpeg b/frontend/src/assets/images/background.jpeg new file mode 100644 index 0000000..0a20fe8 Binary files /dev/null and b/frontend/src/assets/images/background.jpeg differ diff --git a/frontend/src/assets/images/gh-mark.png b/frontend/src/assets/images/gh-mark.png new file mode 100644 index 0000000..081af6c Binary files /dev/null and b/frontend/src/assets/images/gh-mark.png differ diff --git a/frontend/src/assets/images/justicetocat.jpg b/frontend/src/assets/images/justicetocat.jpg new file mode 100644 index 0000000..9e253d4 Binary files /dev/null and b/frontend/src/assets/images/justicetocat.jpg differ diff --git a/frontend/src/assets/images/mona-lovelace.jpg b/frontend/src/assets/images/mona-lovelace.jpg new file mode 100644 index 0000000..e050db0 Binary files /dev/null and b/frontend/src/assets/images/mona-lovelace.jpg differ diff --git a/frontend/src/assets/images/section-background.svg b/frontend/src/assets/images/section-background.svg new file mode 100644 index 0000000..76c44db --- /dev/null +++ b/frontend/src/assets/images/section-background.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + diff --git a/frontend/src/assets/logo.svg b/frontend/src/assets/logo.svg new file mode 100644 index 0000000..7565660 --- /dev/null +++ b/frontend/src/assets/logo.svg @@ -0,0 +1 @@ + diff --git a/frontend/src/assets/main.css b/frontend/src/assets/main.css new file mode 100644 index 0000000..4429558 --- /dev/null +++ b/frontend/src/assets/main.css @@ -0,0 +1,35 @@ +@import './base.css'; + +#app { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + + font-weight: normal; +} + +a, +.green { + text-decoration: none; + color: hsla(160, 100%, 37%, 1); + transition: 0.4s; +} + +@media (hover: hover) { + a:hover { + background-color: hsla(160, 100%, 37%, 0.2); + } +} + +@media (min-width: 1024px) { + body { + display: flex; + place-items: center; + } + + #app { + display: grid; + grid-template-columns: 1fr 1fr; + padding: 0 2rem; + } +} \ No newline at end of file diff --git a/frontend/src/components/AuthorizationCallback.vue b/frontend/src/components/AuthorizationCallback.vue new file mode 100644 index 0000000..67e114a --- /dev/null +++ b/frontend/src/components/AuthorizationCallback.vue @@ -0,0 +1,53 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/Gallery.vue b/frontend/src/components/Gallery.vue new file mode 100644 index 0000000..c296b7a --- /dev/null +++ b/frontend/src/components/Gallery.vue @@ -0,0 +1,449 @@ + + + + + diff --git a/frontend/src/components/Login.vue b/frontend/src/components/Login.vue new file mode 100644 index 0000000..77e1d71 --- /dev/null +++ b/frontend/src/components/Login.vue @@ -0,0 +1,118 @@ + + + + + + + + \ No newline at end of file diff --git a/frontend/src/components/Logout.vue b/frontend/src/components/Logout.vue new file mode 100644 index 0000000..385c1ea --- /dev/null +++ b/frontend/src/components/Logout.vue @@ -0,0 +1,11 @@ + + \ No newline at end of file diff --git a/frontend/src/components/NotFound.vue b/frontend/src/components/NotFound.vue new file mode 100644 index 0000000..f90b72a --- /dev/null +++ b/frontend/src/components/NotFound.vue @@ -0,0 +1,21 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/main.js b/frontend/src/main.js new file mode 100644 index 0000000..82977ad --- /dev/null +++ b/frontend/src/main.js @@ -0,0 +1,35 @@ +//import './assets/main.css' +import 'bootstrap/dist/css/bootstrap.css' +import "bootstrap/dist/js/bootstrap.js" +import { createApp } from 'vue' +import { createStore } from 'vuex' +import { createPinia, defineStore } from 'pinia' +import Axios from 'axios' + + +import App from './App.vue' +import router from './router' +import store from './stores/store' + +const app = createApp(App) + + +Axios.interceptors.request.use(function (config) { + const token = store.getters.token + if (token != '') { + config.headers.Authorization = `Bearer ${token}` + } else { + config.headers.Authorization = '' + } + + return config; + }); + + +app.use(createPinia()) +app.use(store) +app.use(router) + + +app.mount('#app') + diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js new file mode 100644 index 0000000..9fe56c8 --- /dev/null +++ b/frontend/src/router/index.js @@ -0,0 +1,97 @@ +import { createRouter, createWebHistory } from 'vue-router' +import store from '../stores/store' +import NotFoundView from '../views/NotFoundView.vue' +import LoginView from '../views/LoginView.vue' +import LogoutView from '../views/LogoutView.vue' +import AuthorizationCallback from '../views/AuthorizationCallbackView.vue' +import Gallery from '../views/GalleryView.vue' + + +// const router = new VueRouter({ +// mode: 'history', +// routes: [ +// { path: '/', redirect: '/gallery'}, +// { path: '/login', name: 'Login', component: Login }, +// { path: '/login/callback', name: "AuthorizationCallback", component: AuthorizationCallback }, +// { path: '/logout', name: 'Logout', component: Logout}, +// { path: '/gallery', name: 'Gallery', component: Gallery, meta: { requiresAuth: true } }, +// { path: '*', component: NotFound } +// ] +// }) + + +const router = createRouter({ + history: createWebHistory(import.meta.env.BASE_URL), + routes: [ + { path: '/', redirect: '/login'}, + { path: '/login/callback', name: "AuthorizationCallback", component: AuthorizationCallback }, + { path: '/gallery', name: 'Gallery', component: Gallery, meta: { requiresAuth: true } }, + { path: '/logout', name: 'Logout', component: LogoutView }, + // { + // path: '/login/callback', + // name: 'home', + // component: HomeView + // }, + // { + // path: '/about', + // name: 'about', + // // route level code-splitting + // // this generates a separate chunk (About.[hash].js) for this route + // // which is lazy-loaded when the route is visited. + // component: () => import('../views/AboutView.vue') + // }, + { + path: '/login', + name: 'Login', + component: LoginView + }, + { + path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFoundView + } + + + ] +}) + +const jwt = { + decode(token) { + if (!token) return {} + const claimset = token.split('.', 3)[1] + return JSON.parse(atob(claimset)) + }, + isExpired(token){ + const claimset = this.decode(token) + console.log("Claimset:", claimset) + const exp = claimset['exp'] + if (exp === undefined) { + return false + } + console.log("Token expiration time since epoch:", exp) + const nowLocal = new Date() + const nowLocalInSecondsSincEpoch = Math.floor(nowLocal.getTime()/1000) + console.log("Local time since epoch:", nowLocal.getTime()) + const nowUTCInSecondsSincEpoch = nowLocalInSecondsSincEpoch + nowLocal.getTimezoneOffset() * 60 + console.log("UTC time since epoch:", nowUTCInSecondsSincEpoch) + console.log("Expiration delta: ", nowUTCInSecondsSincEpoch - exp) + return exp <= nowUTCInSecondsSincEpoch + } +} + + +router.beforeEach((to, from, next) => { + console.log('beforeEach', to, from) + if (to.matched.some(record => record.meta.requiresAuth)) { + if (store.getters.isLoggedIn && !jwt.isExpired(store.getters.token)) { + next() + return + } + next({ path: '/login', query: { returnUrl: to.path } }) + } + else { + next() + } +}) + +export default router + + diff --git a/frontend/src/stores/counter.js b/frontend/src/stores/counter.js new file mode 100644 index 0000000..b6757ba --- /dev/null +++ b/frontend/src/stores/counter.js @@ -0,0 +1,12 @@ +import { ref, computed } from 'vue' +import { defineStore } from 'pinia' + +export const useCounterStore = defineStore('counter', () => { + const count = ref(0) + const doubleCount = computed(() => count.value * 2) + function increment() { + count.value++ + } + + return { count, doubleCount, increment } +}) diff --git a/frontend/src/stores/store.js b/frontend/src/stores/store.js new file mode 100644 index 0000000..400f5b6 --- /dev/null +++ b/frontend/src/stores/store.js @@ -0,0 +1,75 @@ +import { createStore } from 'vuex' +import Axios from 'axios' + +const store = createStore({ + id: 'gallery', + state: () => ({ + token: localStorage.getItem("token") || '', + nonce: null, + gallery: null + }), + mutations: { + token(state, token) { + state.token = token + }, + nonce(state, nonce) { + state.nonce = nonce + } + }, + actions: { + // authenticate({ commit }, {code, state}) { + authenticate({ commit }, {code}) { + return new Promise((resolve, reject) => { + Axios.get(`http://localhost:5000/authenticate/${code}`).then((response) => { + if (response.data.token) { + const token = response.data.token + console.log("Authenticated with token", token) + commit('token', token ) + localStorage.setItem('token', token) + + // const [nonce, returnUrl] = atob(state).split(':', 2) + // console.log("Received authentication state with nonce", nonce, "and return url", returnUrl) + const returnUrl = "http://localhost:5173/gallery"; //change back to 8080 + resolve(returnUrl) + } else { + reject(response.data.error) + } + }).catch((e) => { + reject(e) + }) + }) + }, + logout({ commit }) { + return new Promise((resolve) => { + console.log("Logout") + commit('token', null) + localStorage.removeItem('token') + resolve() + }) + }, + nonce({commit}) { + return new Promise((resolve) => { + var stateArray = new Uint8Array(16); + window.crypto.getRandomValues(stateArray); + // First convert a typed array to normal array before transforming into a hex string. + // Otherwise, we will loose randomness due to the wrapping of Uint8. + const nonce = Array.from(stateArray) + .map((i) => ("0" + i.toString(16)).slice(-2)) + .join(""); + + commit('nonce', nonce) + resolve(nonce) + }) + } + + }, + getters: { + isLoggedIn: state => !!state.token, + profile: state => jwt.decode(state.token)['profile'], + token: state => state.token, + nonce: state => state.nonce, + + } + }) + + export default store \ No newline at end of file diff --git a/frontend/src/views/AuthorizationCallbackView.vue b/frontend/src/views/AuthorizationCallbackView.vue new file mode 100644 index 0000000..f39c06b --- /dev/null +++ b/frontend/src/views/AuthorizationCallbackView.vue @@ -0,0 +1,10 @@ + + + diff --git a/frontend/src/views/GalleryView.vue b/frontend/src/views/GalleryView.vue new file mode 100644 index 0000000..28c426d --- /dev/null +++ b/frontend/src/views/GalleryView.vue @@ -0,0 +1,11 @@ + + + diff --git a/frontend/src/views/LoginView.vue b/frontend/src/views/LoginView.vue new file mode 100644 index 0000000..9d05b55 --- /dev/null +++ b/frontend/src/views/LoginView.vue @@ -0,0 +1,12 @@ + + + diff --git a/frontend/src/views/LogoutView.vue b/frontend/src/views/LogoutView.vue new file mode 100644 index 0000000..2c3af7b --- /dev/null +++ b/frontend/src/views/LogoutView.vue @@ -0,0 +1,10 @@ + + + diff --git a/frontend/src/views/NotFoundView.vue b/frontend/src/views/NotFoundView.vue new file mode 100644 index 0000000..903beb3 --- /dev/null +++ b/frontend/src/views/NotFoundView.vue @@ -0,0 +1,10 @@ + + + diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..5c45e1d --- /dev/null +++ b/frontend/vite.config.js @@ -0,0 +1,16 @@ +import { fileURLToPath, URL } from 'node:url' + +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + vue(), + ], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)) + } + } +}) diff --git a/gallery/.gitignore b/gallery/.gitignore new file mode 100644 index 0000000..8dcca54 --- /dev/null +++ b/gallery/.gitignore @@ -0,0 +1,31 @@ +*.db + +# Created by https://www.toptal.com/developers/gitignore/api/go +# Edit at https://www.toptal.com/developers/gitignore?templates=go + +### Go ### +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out +*.idea +*.vscode + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work + +# End of https://www.toptal.com/developers/gitignore/api/go diff --git a/gallery/config.json b/gallery/config.json new file mode 100644 index 0000000..71b2cc3 --- /dev/null +++ b/gallery/config.json @@ -0,0 +1,9 @@ +{ + "host": "localhost", + "port": 8081, + "secret": "secretsecret1234secretsecret1234", + "database": "./galleries.db", + "allowed-origins": [ + "http://localhost:5173" + ] +} \ No newline at end of file diff --git a/gallery/go.mod b/gallery/go.mod new file mode 100644 index 0000000..7f0dd27 --- /dev/null +++ b/gallery/go.mod @@ -0,0 +1,9 @@ +module github.com/gallery-service + +go 1.20 + +require ( + github.com/dgrijalva/jwt-go v3.2.0+incompatible + github.com/gorilla/mux v1.8.0 + github.com/mattn/go-sqlite3 v1.14.4 +) diff --git a/gallery/go.sum b/gallery/go.sum new file mode 100644 index 0000000..e2cde64 --- /dev/null +++ b/gallery/go.sum @@ -0,0 +1,6 @@ +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/mattn/go-sqlite3 v1.14.4 h1:4rQjbDxdu9fSgI/r3KN72G3c2goxknAqHHgPWWs8UlI= +github.com/mattn/go-sqlite3 v1.14.4/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI= diff --git a/gallery/main.go b/gallery/main.go new file mode 100644 index 0000000..595d15a --- /dev/null +++ b/gallery/main.go @@ -0,0 +1,726 @@ +package main + +import ( + "database/sql" + "encoding/json" + "errors" + "fmt" + "log" + "net/http" + "os" + "strconv" + "strings" + + "github.com/dgrijalva/jwt-go" + "github.com/gorilla/mux" + _ "github.com/mattn/go-sqlite3" +) + +type Configuration struct { + Host string `json:"host"` + Port int `json:"port"` + + Database string `json:"database"` + Secret string `json:"secret"` + AllowedOrigins []string `json:"allowed-origins"` +} + +var configuration *Configuration + +func LoadConfiguration(path string) (*Configuration, error) { + configuration := &Configuration{} + + file, err := os.Open(path) + if err != nil { + return nil, err + } + defer file.Close() + + err = json.NewDecoder(file).Decode(configuration) + if err != nil { + return nil, err + } + + return configuration, nil +} + +var db *sql.DB + +func GetDb() *sql.DB { + if db != nil { + return db + } + + db, err := sql.Open("sqlite3", configuration.Database) + if err != nil { + panic(err) + } + + return db +} + +func InitializeDb() { + db := GetDb() + + tables := [...]string{ + `CREATE TABLE IF NOT EXISTS 'gallery' + (id INTEGER PRIMARY KEY AUTOINCREMENT, login TEXT UNIQUE, title TEXT NOT NULL, + description TEXT, created_at DATETIME, updated_at DATETIME);`, + `CREATE TRIGGER IF NOT EXISTS 'gallery_after_insert' + AFTER INSERT ON 'gallery' + BEGIN + UPDATE 'gallery' + SET + created_at = DATETIME('NOW'), + updated_at = DATETIME('NOW'); + END`, + `CREATE TRIGGER IF NOT EXISTS 'gallery_after_update' + AFTER UPDATE ON 'gallery' + BEGIN + UPDATE 'gallery' + SET + updated_at = DATETIME('NOW'); + END`, + `CREATE TABLE IF NOT EXISTS 'art_piece' + (id INTEGER PRIMARY KEY AUTOINCREMENT, gallery_id INTEGER, uri TEXT, title TEXT, + description TEXT, is_file_upload BOOL DEFAULT 0, stars INTEGER CHECK (stars BETWEEN 0 AND 3) DEFAULT 0, created_at DATETIME, updated_at DATETIME, + FOREIGN KEY(gallery_id) REFERENCES gallery(id) ON DELETE CASCADE);`, + `CREATE TRIGGER IF NOT EXISTS 'art_piece_after_insert' + AFTER INSERT ON 'art_piece' + BEGIN + UPDATE 'art_piece' + SET + created_at = DATETIME('NOW'), + updated_at = DATETIME('NOW'); + END`, + `CREATE TRIGGER IF NOT EXISTS 'art_piece_after_update' + AFTER UPDATE ON 'art_piece' + BEGIN + UPDATE 'art_piece' + SET + updated_at = DATETIME('NOW'); + END`, + } + + for _, q := range tables { + _, err := db.Exec(q) + if err != nil { + log.Printf("Failed query: %s", q) + panic(err) + } + } +} + +func Contains(needle string, haystack *[]string) bool { + for _, candidate := range *haystack { + if needle == candidate { + return true + } + } + + return false +} + +func SetCORSPolicy(w http.ResponseWriter, r *http.Request) { + origin := r.Header.Get("Origin") + w.Header().Set("Access-Control-Allow-Headers", "authorization") + if Contains(origin, &configuration.AllowedOrigins) { + w.Header().Set("Access-Control-Allow-Origin", origin) + return + } + + w.Header().Set("Access-Control-Allow-Origin", "*") +} + +type Gallery struct { + ID int64 `json:"id"` + Title string `json:"title"` + Description string `json:"description"` +} + +func (g *Gallery) Get(profile *OctoProfile) error { + db := GetDb() + + stmt, err := db.Prepare(`SELECT id, title, description FROM gallery WHERE login = ?`) + if err != nil { + return err + } + defer stmt.Close() + + rs, err := stmt.Query(profile.Login) + if err != nil { + return err + } + defer rs.Close() + + if rs.Next() { + rs.Scan(&g.ID, &g.Title, &g.Description) + + return nil + } else { + g.ID = -1 + g.Title = "The Empty gallery" + g.Description = "This gallery is in desparate need of some art!" + + return g.Create(profile) + } +} + +func (g *Gallery) Create(profile *OctoProfile) error { + db := GetDb() + + stmt, err := db.Prepare("INSERT INTO gallery (login, title, description) VALUES(?,?,?)") + if err != nil { + return err + } + defer stmt.Close() + + r, err := stmt.Exec(profile.Login, g.Title, g.Description) + if err != nil { + return err + } + + if i, err := r.RowsAffected(); err != nil || i != 1 { + return errors.New("Unable to create gallery") + } + + newId, err := r.LastInsertId() + if err != nil { + return err + } + g.ID = newId + + return nil +} + +func (g Gallery) Update(profile *OctoProfile) error { + db := GetDb() + query := fmt.Sprintf("UPDATE gallery SET title = '%s', description = '%s' WHERE id = %d and login = '%s'", g.Title, g.Description, g.ID, profile.Login) + + stmt, err := db.Prepare(query) + if err != nil { + return err + } + defer stmt.Close() + + r, err := stmt.Exec() + if err != nil { + return err + } + + if i, err := r.RowsAffected(); err != nil || i != 1 { + return errors.New("Unable to update gallery") + } + + return nil +} + +func (g Gallery) GetArtPiece(id int64) (*ArtPiece, error) { + db := GetDb() + + stmt, err := db.Prepare(`SELECT id, title, description, stars, uri, is_file_upload FROM art_piece WHERE gallery_id = ? AND id = ?`) + if err != nil { + return nil, err + } + defer stmt.Close() + + rs, err := stmt.Query(g.ID, id) + if err != nil { + return nil, err + } + defer rs.Close() + + if rs.Next() { + art_piece := &ArtPiece{} + rs.Scan(&art_piece.ID, &art_piece.Title, &art_piece.Description, &art_piece.Stars, &art_piece.Uri, &art_piece.IsFileUpload) + + return art_piece, nil + } + + return nil, errors.New("Art piece seems to be missing!") +} + +func (g Gallery) GetAllArtPieces() ([]ArtPiece, error) { + db := GetDb() + + stmt, err := db.Prepare(`SELECT id, title, description, stars, uri, is_file_upload FROM art_piece WHERE gallery_id = ?`) + if err != nil { + return nil, err + } + defer stmt.Close() + + rs, err := stmt.Query(g.ID) + if err != nil { + return nil, err + } + defer rs.Close() + art_pieces := []ArtPiece{} + + for rs.Next() { + art_piece := &ArtPiece{} + rs.Scan(&art_piece.ID, &art_piece.Title, &art_piece.Description, &art_piece.Stars, &art_piece.Uri, &art_piece.IsFileUpload) + + art_pieces = append(art_pieces, *art_piece) + } + + return art_pieces, nil +} + +type ArtPiece struct { + ID int64 `json:"id"` + Title string `json:"title"` + Description string `json:"description"` + Stars int `json:"stars"` + Uri string `json:"uri"` + IsFileUpload bool `json:"is_file_upload"` +} + +func (p *ArtPiece) Create(gallery Gallery) error { + db := GetDb() + + stmt, err := db.Prepare("INSERT INTO art_piece (title, description, uri, gallery_id, is_file_upload) VALUES(?,?,?,?,?)") + if err != nil { + return err + } + defer stmt.Close() + + r, err := stmt.Exec(p.Title, p.Description, p.Uri, gallery.ID, p.IsFileUpload) + if err != nil { + return err + } + + if i, err := r.RowsAffected(); err != nil || i != 1 { + return errors.New("Unable to create art piece") + } + + newId, err := r.LastInsertId() + if err != nil { + return err + } + p.ID = newId + + return nil +} + +func (p ArtPiece) Update(gallery Gallery) error { + db := GetDb() + + query := fmt.Sprintf("UPDATE art_piece SET title = '%s', description = '%s', stars = '%d', uri = '%s' WHERE id = %d and gallery_id = '%d'", p.Title, p.Description, p.Stars, p.Uri, p.ID, gallery.ID) + stmt, err := db.Prepare(query) + + if err != nil { + return err + } + defer stmt.Close() + + r, err := stmt.Exec() + if err != nil { + return err + } + + if i, err := r.RowsAffected(); err != nil || i != 1 { + return errors.New("Unable to update art piece") + } + + return nil +} + +func (p ArtPiece) Delete(gallery Gallery) error { + db := GetDb() + + stmt, err := db.Prepare("DELETE FROM art_piece WHERE id = ? and gallery_id = ?") + if err != nil { + return err + } + defer stmt.Close() + + r, err := stmt.Exec(p.ID, gallery.ID) + if err != nil { + return err + } + + if i, err := r.RowsAffected(); err != nil || i != 1 { + return errors.New("Unable to delete art piece") + } + + return nil +} + +func GetProfile(r *http.Request) *OctoProfile { + profile := new(OctoProfile) + + profile.Name = r.Header.Get(GitHubNameHeader.String()) + profile.Email = r.Header.Get(GitHubEmailHeader.String()) + profile.Login = profile.Email + + return profile +} + +func GalleryHandler(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + GetGalleryHandler(w, r) + case http.MethodPut: + UpdateGalleryHandler(w, r) + case http.MethodOptions: + return + default: + http.Error(w, "Method not permitted", http.StatusBadRequest) + } +} + +func UpdateGalleryHandler(w http.ResponseWriter, r *http.Request) { + profile := GetProfile(r) + + var gallery Gallery + + err := json.NewDecoder(r.Body).Decode(&gallery) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + err = gallery.Update(profile) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) +} + +func GetGalleryHandler(w http.ResponseWriter, r *http.Request) { + profile := GetProfile(r) + var gallery Gallery + + log.Printf("hello") + + err := gallery.Get(profile) + + if err == nil { + json.NewEncoder(w).Encode(gallery) + } else { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +func GalleryArtsHandler(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + GetGalleryAllArtHandler(w, r) + case http.MethodPost: + AddGalleryArtHandler(w, r) + case http.MethodOptions: + return + default: + http.Error(w, "Method not permitted", http.StatusBadRequest) + } +} + +func GetGalleryAllArtHandler(w http.ResponseWriter, r *http.Request) { + profile := GetProfile(r) + var gallery Gallery + + err := gallery.Get(profile) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + art_pieces, err := gallery.GetAllArtPieces() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + json.NewEncoder(w).Encode(art_pieces) +} + +func AddGalleryArtHandler(w http.ResponseWriter, r *http.Request) { + profile := GetProfile(r) + var gallery Gallery + var art_piece ArtPiece + + err := gallery.Get(profile) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if gallery.Title == "The Empty gallery" { + gallery.Title = "This gallery belongs to " + profile.Name + gallery.Description = "Mona loves your art!" + err := gallery.Update(profile) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + } + + err = json.NewDecoder(r.Body).Decode(&art_piece) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + err = art_piece.Create(gallery) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + w.WriteHeader(http.StatusOK) +} + +func GalleryArtHandler(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + GetGalleryArtHandler(w, r) + case http.MethodPut: + UpdateGalleryArtHandler(w, r) + case http.MethodDelete: + DeleteGalleryArtHandler(w, r) + case http.MethodOptions: + return + default: + http.Error(w, "Method not permitted", http.StatusBadRequest) + } +} + +func GetGalleryArtHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + art_piece_id, err := strconv.ParseInt(vars["id"], 10, 64) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + profile := GetProfile(r) + var gallery Gallery + + err = gallery.Get(profile) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + art_pieces, err := gallery.GetArtPiece(art_piece_id) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + json.NewEncoder(w).Encode(art_pieces) +} + +func UpdateGalleryArtHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + art_piece_id, err := strconv.ParseInt(vars["id"], 10, 64) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + profile := GetProfile(r) + var gallery Gallery + + err = gallery.Get(profile) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + old_art_piece, err := gallery.GetArtPiece(art_piece_id) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + var art_piece ArtPiece + err = json.NewDecoder(r.Body).Decode(&art_piece) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + art_piece.ID = old_art_piece.ID + + err = art_piece.Update(gallery) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) +} + +func DeleteGalleryArtHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + art_piece_id, err := strconv.ParseInt(vars["id"], 10, 64) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + profile := GetProfile(r) + var gallery Gallery + + err = gallery.Get(profile) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + art_piece, err := gallery.GetArtPiece(art_piece_id) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + err = art_piece.Delete(gallery) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) +} + +func HomeLinkHandler(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Welcome home!") +} + +type OctoProfile struct { + Login string `json:"login"` + Name string `json:"name"` + Email string `json:"email"` +} + +type OctoClaims struct { + jwt.StandardClaims + + Profile OctoProfile `json:"profile"` +} + +func (claims OctoClaims) Valid() error { + + log.Printf("Validating standard claims") + if err := claims.StandardClaims.Valid(); err != nil { + log.Printf("Failed standard claim validation with error %s", err) + return err + } + + vErr := new(jwt.ValidationError) + + log.Printf("Validating private claims") + if claims.Issuer != "OctoGallery" { + log.Printf("Invalid issuer: %s", claims.Issuer) + vErr.Inner = errors.New("Invalid issuer!") + vErr.Errors |= jwt.ValidationErrorIssuer + } + + if vErr.Errors == 0 { + log.Printf("Validated all claims without errors.") + return nil + } + + return vErr +} + +type ProfileHeader int + +const ( + GitHubLoginHeader ProfileHeader = iota + GitHubNameHeader + GitHubEmailHeader +) + +func (p ProfileHeader) String() string { + return [...]string{"X-GitHub-Login", "X-GitHub-Name", "X-GitHub-Email"}[p] +} + +func authnMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + SetCORSPolicy(w, r) + + if r.Method == http.MethodOptions { + next.ServeHTTP(w, r) + return + } + + r.Header.Del(GitHubLoginHeader.String()) + r.Header.Del(GitHubNameHeader.String()) + r.Header.Del(GitHubEmailHeader.String()) + + authz := r.Header.Get("Authorization") + + if strings.HasPrefix(authz, "Bearer") { + tokenString := strings.TrimSpace(strings.TrimPrefix(authz, "Bearer")) + log.Printf("AuthN: Received bearer token %s", tokenString) + token, err := jwt.ParseWithClaims(tokenString, &OctoClaims{}, func(token *jwt.Token) (interface{}, error) { + // Don't forget to validate the alg is what you expect: + //if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + // return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) + //} + + // hmacSampleSecret is a []byte containing your secret, e.g. []byte("my_secret_key") + return []byte(configuration.Secret), nil + }) + + if err != nil { + log.Printf("AuthN: Invalid token %s", err) + http.Error(w, "Forbidden", http.StatusForbidden) + return + } + + if claims, ok := token.Claims.(*OctoClaims); ok && token.Valid { + log.Printf("AuthN: Received valid token %s", authz) + + log.Printf("AuthN: Adding %s %s", GitHubLoginHeader, claims.Profile.Login) + r.Header.Add(GitHubLoginHeader.String(), claims.Profile.Login) + log.Printf("AuthN: Adding %s %s", GitHubNameHeader, claims.Profile.Name) + r.Header.Add(GitHubNameHeader.String(), claims.Profile.Name) + log.Printf("AuthN: Adding %s %s", GitHubEmailHeader, claims.Profile.Email) + r.Header.Add(GitHubEmailHeader.String(), claims.Profile.Email) + next.ServeHTTP(w, r) + return + } else { + log.Printf("AuthN: Received token with invalid claims") + http.Error(w, "Forbidden", http.StatusForbidden) + return + } + } + log.Printf("AuthN: Received invalid authorization value") + http.Error(w, "Forbidden", http.StatusForbidden) + + }) +} + +func main() { + + if len(os.Args) != 2 { + fmt.Printf("Usage %s CONFIG_FILE\n", os.Args[0]) + return + } + + c, err := LoadConfiguration(os.Args[1]) + if err != nil { + panic(err) + } + configuration = c + + InitializeDb() + + router := mux.NewRouter().StrictSlash(true) + router.Use(mux.CORSMethodMiddleware(router)) + router.Use(authnMiddleware) + router.HandleFunc("/", HomeLinkHandler) + router.HandleFunc("/gallery", GalleryHandler).Methods(http.MethodGet, http.MethodPut, http.MethodOptions) + router.HandleFunc("/gallery/art", GalleryArtsHandler).Methods(http.MethodGet, http.MethodPost, http.MethodOptions) + router.HandleFunc("/gallery/art/{id}", GalleryArtHandler).Methods(http.MethodGet, http.MethodPut, http.MethodDelete, http.MethodOptions) + + addr := fmt.Sprintf(":%d", configuration.Port) + log.Printf("Listening on %s", addr) + log.Fatal(http.ListenAndServe(addr, router)) +} diff --git a/passwords.txt b/passwords.txt new file mode 100644 index 0000000..7b29e2a --- /dev/null +++ b/passwords.txt @@ -0,0 +1,2 @@ +user=alice@localhost +password=verysecure diff --git a/queries/qlpack.yml b/queries/qlpack.yml new file mode 100644 index 0000000..ba09081 --- /dev/null +++ b/queries/qlpack.yml @@ -0,0 +1,11 @@ +--- +library: false +warnOnImplicitThis: false +name: queries +version: 0.0.1 +dependencies: + codeql/javascript-all: "*" + + + + diff --git a/queries/vue-xss.ql b/queries/vue-xss.ql new file mode 100644 index 0000000..d9a7ba9 --- /dev/null +++ b/queries/vue-xss.ql @@ -0,0 +1,118 @@ +/** + * @name Client-side cross-site scripting + * @description Writing user input directly to the DOM allows for + * a cross-site scripting vulnerability. + * @kind path-problem + * @problem.severity error + * @security-severity 6.1 + * @precision high + * @id js/vue-xss + * @tags security + * external/cwe/cwe-079 + * external/cwe/cwe-116 + */ + +import javascript +import semmle.javascript.security.dataflow.DomBasedXssQuery +import semmle.javascript.security.dataflow.DomBasedXssCustomizations +import DataFlow::PathGraph + +// Unprecise Axios Source +class AxiosSource extends DomBasedXss::Source { + AxiosSource() { + this.(DataFlow::PropRead).getPropertyName() = "data" and + exists(DataFlow::ParameterNode response | response.getName() = "response" | + response.flowsTo(this.(DataFlow::PropRead).getBase()) + ) + } +} + +class VForAttribute extends DataFlow::Node { + HTML::Attribute attr; + + VForAttribute() { + this.(DataFlow::HtmlAttributeNode).getAttribute() = attr and attr.getName() = "v-for" + } + + /** + * Gets the HTML attribute of this sink. + */ + HTML::Attribute getAttr() { result = attr } + + string getForAlias() { result = attr.getValue().regexpCapture("(.*)\\s+in\\s+(.*)", 1).trim() } + + string getForArraySource() { + result = attr.getValue().regexpCapture("(.*)\\s+in\\s+(.*)", 2).trim() + } + + Vue::Component getComponent() { + result.getTemplateElement().(Vue::Template::HtmlElement).getElement() = this.getAttr().getRoot() + } + + DataFlow::Node getAForArraySourceValue() { + exists(DataFlow::PropWrite propWrite | + resolveRefForArraySourceValue(this, this.getForArraySource(), propWrite) and + result = propWrite.getRhs() + ) + } +} + +predicate resolveRefForArraySourceValue( + VForAttribute vfor, string accessPath, DataFlow::PropRef propRef +) { + // accesssPath must be a prefix of the for array source. + exists(int dotOffsets, string arraySrc | + arraySrc = vfor.getForArraySource() and dotOffsets = arraySrc.indexOf(".") + | + ( + arraySrc.prefix(dotOffsets) = accessPath + or + arraySrc = accessPath + ) and + accessPath != "this" + ) and + ( + // Base case: foo or this.foo + exists(string prop | prop = accessPath.replaceAll("?", "").replaceAll("this.", "") | + propRef = vfor.getComponent().getAnInstanceRef().getAPropertyReference(prop) + ) + or + // Induction step: foo.bar or this.foo.bar + exists(DataFlow::PropRef basePropRef, string baseAccessPath, string prop | + baseAccessPath = accessPath.prefix(max(int idx | idx = accessPath.indexOf("."))) and + prop = accessPath.suffix(max(int idx | idx = accessPath.indexOf(".")) + 1).replaceAll("?", "") and + resolveRefForArraySourceValue(vfor, baseAccessPath, basePropRef) and + propRef.getPropertyName() = prop and + ( + propRef.getBase() = basePropRef + or + // A more precise source can be identified when the source of this.gallery, reponse.data, is flowing through an intermediate variable + // to the base of a property reference where the property name matches our current property. + // e.g., + // const gallery = response.data + // axios.get("http://localhost:8081/gallery/art").then((response) => { + // gallery.art = response.data + // this.gallery = gallery + basePropRef.(DataFlow::PropWrite).getRhs().getALocalSource().flowsTo(propRef.getBase()) + ) + ) + ) +} + +class VHtmlAttributeStep extends TaintTracking::SharedTaintStep { + override predicate viewComponentStep(DataFlow::Node pred, DataFlow::Node succ) { + succ.(VForAttribute).getAForArraySourceValue() = pred + or + exists(VForAttribute vfor, Vue::VHtmlAttribute vhtml | pred = vfor and succ = vhtml | + vfor.getAttr().getElement() = vhtml.getAttr().getElement().getParent+() and + vhtml.getAttr().getValue().prefix(vhtml.getAttr().getValue().indexOf(".", 0, 0)) = + vfor.getForAlias() + ) + } +} + +from DataFlow::Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink +where cfg.hasFlowPath(source, sink) +select sink.getNode(), source, sink, + sink.getNode().(Sink).getVulnerabilityKind() + " vulnerability due to $@.", source.getNode(), + "user-provided value" diff --git a/storage/.dockerignore b/storage/.dockerignore new file mode 100644 index 0000000..9f97022 --- /dev/null +++ b/storage/.dockerignore @@ -0,0 +1 @@ +target/ \ No newline at end of file diff --git a/storage/.gitignore b/storage/.gitignore new file mode 100644 index 0000000..6314db0 --- /dev/null +++ b/storage/.gitignore @@ -0,0 +1,49 @@ +# Created by https://www.toptal.com/developers/gitignore/api/java,maven +# Edit at https://www.toptal.com/developers/gitignore?templates=java,maven + +### Java ### +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +replay_pid* + +### Maven ### +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +# https://github.com/takari/maven-wrapper#usage-without-binary-jar +.mvn/wrapper/maven-wrapper.jar + +# Eclipse m2e generated files +# Eclipse Core +.project +# JDT-specific (Eclipse Java Development Tools) +.classpath + +# End of https://www.toptal.com/developers/gitignore/api/java,maven diff --git a/storage/pom.xml b/storage/pom.xml new file mode 100644 index 0000000..047969f --- /dev/null +++ b/storage/pom.xml @@ -0,0 +1,84 @@ + + + + 4.0.0 + + com.github.advancedsecurity + storage + 23.0.0-SNAPSHOT + storage + Demo project for Spring Boot + + + 11 + + + + org.springframework.boot + spring-boot-starter-parent + 2.3.3.RELEASE + + + + + + io.minio + minio + 7.1.4 + + + org.springframework.boot + spring-boot-starter-web + + + + + org.projectlombok + lombok + 1.18.30 + provided + + + + + jar + + + ${project.name} + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/storage/src/main/java/com/github/advancedsecurity/storageservice/App.java b/storage/src/main/java/com/github/advancedsecurity/storageservice/App.java new file mode 100644 index 0000000..3cb3642 --- /dev/null +++ b/storage/src/main/java/com/github/advancedsecurity/storageservice/App.java @@ -0,0 +1,13 @@ +package com.github.advancedsecurity.storageservice; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class App { + + public static void main(String[] args) { + SpringApplication.run(App.class, args); + } + +} diff --git a/storage/src/main/java/com/github/advancedsecurity/storageservice/WebSecurityConfig.java b/storage/src/main/java/com/github/advancedsecurity/storageservice/WebSecurityConfig.java new file mode 100644 index 0000000..5cee2d1 --- /dev/null +++ b/storage/src/main/java/com/github/advancedsecurity/storageservice/WebSecurityConfig.java @@ -0,0 +1,64 @@ +/* +package com.github.advancedsecurity.storageservice; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; + +import com.github.advancedsecurity.storageservice.security.BearerAuthenticationFilter; +// import com.github.advancedsecurity.storageservice.security.JwtAuthenticationEntryPoint; +// import com.github.advancedsecurity.storageservice.security.JwtAuthenticationProvider; +// import com.github.advancedsecurity.storageservice.security.JwtAccessDeniedHandler; +import com.github.advancedsecurity.storageservice.security.JwtAuthenticationSuccessHandler; + +@Configuration +@EnableWebSecurity +public class WebSecurityConfig extends WebSecurityConfigurerAdapter { + private String antPattern = "/**"; + + // @Autowired + // private JwtAuthenticationProvider jwtAuthenticatinProvider; + + // @Autowired + // private JwtAccessDeniedHandler jwtAccessDeniedHandler; + + // @Autowired + // private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; + + @Autowired + private JwtAuthenticationSuccessHandler jwtAuthenticationSuccessHandler; + + @Override + protected void configure(HttpSecurity http) throws Exception { + BearerAuthenticationFilter filter = new BearerAuthenticationFilter(authenticationManager(), this.antPattern); + filter.setAuthenticationSuccessHandler(jwtAuthenticationSuccessHandler); + http.cors().and() + .csrf().disable() + .authorizeRequests().antMatchers(this.antPattern).authenticated() + .and() + // .addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class) + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); + } + + @Override + public void configure(AuthenticationManagerBuilder auth) throws Exception { + // auth.authenticationProvider(jwtAuthenticatinProvider); + } + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration(this.antPattern, new CorsConfiguration().applyPermitDefaultValues()); + return source; + } +} +*/ \ No newline at end of file diff --git a/storage/src/main/java/com/github/advancedsecurity/storageservice/config/MinioConfig.java b/storage/src/main/java/com/github/advancedsecurity/storageservice/config/MinioConfig.java new file mode 100644 index 0000000..479c312 --- /dev/null +++ b/storage/src/main/java/com/github/advancedsecurity/storageservice/config/MinioConfig.java @@ -0,0 +1,40 @@ +package com.github.advancedsecurity.storageservice.config; + +import io.minio.BucketExistsArgs; +import io.minio.MakeBucketArgs; +import io.minio.MinioClient; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class MinioConfig { + + public static final String DEFAULT_BUCKET_NAME = "default"; + + @Value("${minio.url}") + private String minioUrl; + + @Value("${minio.username}") + private String minioUsername; + + @Value("${minio.password}") + private String minioPassword; + + @Bean + public MinioClient minioClient() throws Exception { + MinioClient client = MinioClient.builder() + .endpoint(minioUrl) + .credentials(minioUsername, minioPassword) + .build(); + if (!client.bucketExists(BucketExistsArgs.builder().bucket(DEFAULT_BUCKET_NAME).build())) { + client.makeBucket( + MakeBucketArgs + .builder() + .bucket(DEFAULT_BUCKET_NAME) + .build() + ); + } + return client; + } +} \ No newline at end of file diff --git a/storage/src/main/java/com/github/advancedsecurity/storageservice/controllers/BlobController.java b/storage/src/main/java/com/github/advancedsecurity/storageservice/controllers/BlobController.java new file mode 100644 index 0000000..875f98c --- /dev/null +++ b/storage/src/main/java/com/github/advancedsecurity/storageservice/controllers/BlobController.java @@ -0,0 +1,105 @@ +package com.github.advancedsecurity.storageservice.controllers; + +import java.util.UUID; +import java.util.List; +import java.io.InputStream; + +import io.minio.GetObjectArgs; +import io.minio.MinioClient; +import io.minio.PutObjectArgs; +import io.minio.StatObjectArgs; + +import lombok.RequiredArgsConstructor; + +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.http.HttpStatus; +import org.springframework.web.multipart.MultipartFile; + +import com.github.advancedsecurity.storageservice.config.MinioConfig; +// import com.github.advancedsecurity.storageservice.security.JwtAuthenticationToken; +import com.github.advancedsecurity.storageservice.models.Blob; +import com.github.advancedsecurity.storageservice.models.Profile; + +@CrossOrigin +@RestController +@RequiredArgsConstructor +public class BlobController { + + private final MinioClient minio; + + @Value("${blob.allowed-content-types}") + private List allowedContentTypes; + + @Value("${minio.put-object-part-size}") + private long putObjectPartSize; + + @GetMapping("/blob/{id}") + // public Blob get(@PathVariable UUID id, JwtAuthenticationToken token) { + public Blob get(@PathVariable UUID id) { + // var profile = (Profile)token.getPrincipal(); + try { + // var path = String.format("%s/%s", profile.name, id.toString()); + var path = String.format("%s", id); + + var stat = minio.statObject( + StatObjectArgs + .builder() + .bucket(MinioConfig.DEFAULT_BUCKET_NAME) + .object(path) + .build() + ); + + var is = minio.getObject( + GetObjectArgs + .builder() + .bucket(MinioConfig.DEFAULT_BUCKET_NAME) + .object(path) + .build() + ); + + return new Blob( + stat.contentType(), + is.readAllBytes() + ); + } catch (Exception e) { + throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Failed to retrieve blob!"); + } + } + + @PostMapping("/blob") + // public UUID post(@RequestParam MultipartFile file, @RequestParam(defaultValue="false") boolean isBlob, JwtAuthenticationToken token) { + public UUID post(@RequestParam("file") MultipartFile file) { + // var profile = (Profile)token.getPrincipal(); + try { + var id = UUID.randomUUID(); + + if(!allowedContentTypes.contains(file.getContentType())) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Disallowed content type"); + } + + // var path = String.format("%s/%s", profile.name, id.toString()); + var path = String.format("%s", id); + + minio.putObject( + PutObjectArgs + .builder() + .bucket(MinioConfig.DEFAULT_BUCKET_NAME) + .contentType(file.getContentType()) + .object(path) + .stream(file.getInputStream(), file.getSize(), putObjectPartSize) + .build() + ); + + return id; + } catch (Exception e) { + throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Failed to store blob!"); + } + } +} \ No newline at end of file diff --git a/storage/src/main/java/com/github/advancedsecurity/storageservice/models/Blob.java b/storage/src/main/java/com/github/advancedsecurity/storageservice/models/Blob.java new file mode 100644 index 0000000..4e5c93c --- /dev/null +++ b/storage/src/main/java/com/github/advancedsecurity/storageservice/models/Blob.java @@ -0,0 +1,21 @@ +package com.github.advancedsecurity.storageservice.models; + +import java.util.Base64; + +public class Blob { + private String mimeType; + private String base64EncodedData; + + public Blob(String mimeType, byte[] data) { + this.mimeType = mimeType; + this.base64EncodedData = Base64.getEncoder().encodeToString(data); + } + + public String getMimeType() { + return this.mimeType; + } + + public String getData() { + return this.base64EncodedData; + } +} \ No newline at end of file diff --git a/storage/src/main/java/com/github/advancedsecurity/storageservice/models/Profile.java b/storage/src/main/java/com/github/advancedsecurity/storageservice/models/Profile.java new file mode 100644 index 0000000..e7857c0 --- /dev/null +++ b/storage/src/main/java/com/github/advancedsecurity/storageservice/models/Profile.java @@ -0,0 +1,7 @@ +package com.github.advancedsecurity.storageservice.models; + +public class Profile { + public String login; + public String name; + public String email; +} \ No newline at end of file diff --git a/storage/src/main/java/com/github/advancedsecurity/storageservice/security/BearerAuthenticationFilter.java b/storage/src/main/java/com/github/advancedsecurity/storageservice/security/BearerAuthenticationFilter.java new file mode 100644 index 0000000..b5d1f24 --- /dev/null +++ b/storage/src/main/java/com/github/advancedsecurity/storageservice/security/BearerAuthenticationFilter.java @@ -0,0 +1,66 @@ +// package com.github.advancedsecurity.storageservice.security; + +// import java.io.IOException; + +// import org.slf4j.Logger; +// import org.slf4j.LoggerFactory; +// import javax.servlet.http.HttpServletRequest; +// import javax.servlet.http.HttpServletResponse; +// import javax.servlet.FilterChain; +// import javax.servlet.ServletException; +// import org.springframework.security.core.Authentication; +// import org.springframework.security.core.AuthenticationException; +// import org.springframework.security.authentication.AuthenticationManager; +// import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; +// import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver; +// import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver; +// import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; +// import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +// import org.springframework.beans.factory.annotation.Autowired; + +// public class BearerAuthenticationFilter extends AbstractAuthenticationProcessingFilter { + +// private final Logger logger = LoggerFactory.getLogger(this.getClass()); +// private BearerTokenResolver bearerTokenResolver = new DefaultBearerTokenResolver(); + +// public BearerAuthenticationFilter(AuthenticationManager authenticationManager, String defaultFilterProcessesUrl) { +// super(defaultFilterProcessesUrl); + +// setAuthenticationManager(authenticationManager); +// } + +// @Override +// public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) +// throws AuthenticationException, IOException, ServletException +// { +// logger.debug("Bearer authentication attempt."); +// String token; +// try { +// logger.debug("Resolving bearer token."); +// token = this.bearerTokenResolver.resolve(request); +// } +// catch (OAuth2AuthenticationException invalid) { +// throw new JwtAuthenticationException("Invalid Bearer token!"); +// } + +// if (token == null) { +// throw new JwtAuthenticationException("Invalid Bearer token!"); +// } + +// logger.debug("Constructing Bearer authentication token."); +// BearerTokenAuthenticationToken authenticationRequest = new BearerTokenAuthenticationToken(token); + +// logger.debug("Authenticating with Bearer authentication token."); +// return getAuthenticationManager().authenticate(authenticationRequest); +// } + +// @Override +// protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) +// throws IOException, ServletException { +// super.successfulAuthentication(request, response, chain, authResult); + +// // As this authentication is in HTTP header, after success we need to continue the request normally +// // and return the response as if the resource was not secured at all +// chain.doFilter(request, response); +// } +// } \ No newline at end of file diff --git a/storage/src/main/java/com/github/advancedsecurity/storageservice/security/JwtAccessDeniedHandler.java b/storage/src/main/java/com/github/advancedsecurity/storageservice/security/JwtAccessDeniedHandler.java new file mode 100644 index 0000000..ca20300 --- /dev/null +++ b/storage/src/main/java/com/github/advancedsecurity/storageservice/security/JwtAccessDeniedHandler.java @@ -0,0 +1,22 @@ +// package com.github.advancedsecurity.storageservice.security; + +// import java.io.IOException; + +// import org.slf4j.Logger; +// import org.slf4j.LoggerFactory; +// import javax.servlet.http.HttpServletRequest; +// import javax.servlet.http.HttpServletResponse; +// import javax.servlet.ServletException; +// import org.springframework.stereotype.Component; +// import org.springframework.security.web.access.AccessDeniedHandler; +// import org.springframework.security.access.AccessDeniedException; + +// @Component +// public class JwtAccessDeniedHandler implements AccessDeniedHandler { +// @Override +// public void handle +// (HttpServletRequest request, HttpServletResponse response, AccessDeniedException ex) +// throws IOException, ServletException { +// response.sendError(HttpServletResponse.SC_FORBIDDEN); +// } +// } \ No newline at end of file diff --git a/storage/src/main/java/com/github/advancedsecurity/storageservice/security/JwtAuthenticationEntryPoint.java b/storage/src/main/java/com/github/advancedsecurity/storageservice/security/JwtAuthenticationEntryPoint.java new file mode 100644 index 0000000..45450ce --- /dev/null +++ b/storage/src/main/java/com/github/advancedsecurity/storageservice/security/JwtAuthenticationEntryPoint.java @@ -0,0 +1,17 @@ +// package com.github.advancedsecurity.storageservice.security; + +// import java.io.IOException; + +// import javax.servlet.http.HttpServletRequest; +// import javax.servlet.http.HttpServletResponse; +// import org.springframework.security.core.AuthenticationException; +// import org.springframework.security.web.AuthenticationEntryPoint; +// import org.springframework.stereotype.Component; + +// @Component +// public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { +// @Override +// public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { +// response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized access"); +// } +// } \ No newline at end of file diff --git a/storage/src/main/java/com/github/advancedsecurity/storageservice/security/JwtAuthenticationException.java b/storage/src/main/java/com/github/advancedsecurity/storageservice/security/JwtAuthenticationException.java new file mode 100644 index 0000000..f82fd87 --- /dev/null +++ b/storage/src/main/java/com/github/advancedsecurity/storageservice/security/JwtAuthenticationException.java @@ -0,0 +1,13 @@ +// package com.github.advancedsecurity.storageservice.security; + +// import org.springframework.security.core.AuthenticationException; + +// class JwtAuthenticationException extends AuthenticationException { +// public JwtAuthenticationException(String msg) { +// super(msg); +// } + +// public JwtAuthenticationException(String msg, Throwable t) { +// super(msg, t); +// } +// } \ No newline at end of file diff --git a/storage/src/main/java/com/github/advancedsecurity/storageservice/security/JwtAuthenticationProvider.java b/storage/src/main/java/com/github/advancedsecurity/storageservice/security/JwtAuthenticationProvider.java new file mode 100644 index 0000000..94495f3 --- /dev/null +++ b/storage/src/main/java/com/github/advancedsecurity/storageservice/security/JwtAuthenticationProvider.java @@ -0,0 +1,61 @@ +// package com.github.advancedsecurity.storageservice.security; + +// import javax.crypto.spec.SecretKeySpec; +// import java.nio.charset.StandardCharsets; + +// import org.slf4j.Logger; +// import org.slf4j.LoggerFactory; +// import org.springframework.security.core.Authentication; +// import org.springframework.security.core.AuthenticationException; +// import org.springframework.security.authentication.AbstractAuthenticationToken; +// import org.springframework.security.authentication.AuthenticationProvider; +// import org.springframework.security.authentication.AuthenticationServiceException; +// import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; +// import org.springframework.beans.factory.annotation.Value; +// import org.springframework.stereotype.Component; + +// import io.jsonwebtoken.Jwts; +// import io.jsonwebtoken.Jws; +// import io.jsonwebtoken.Claims; +// import io.jsonwebtoken.lang.Maps; +// import io.jsonwebtoken.JwtException; +// import io.jsonwebtoken.jackson.io.JacksonDeserializer; + +// import com.github.advancedsecurity.storageservice.models.Profile; + +// @Component +// public class JwtAuthenticationProvider implements AuthenticationProvider { +// private final Logger logger = LoggerFactory.getLogger(this.getClass()); + +// @Value("${jwt.secret:secret}") +// private String secret; + +// @Value("${jwt.issuer}") +// private String issuer; + +// @Override +// public boolean supports(Class authentication) { +// return BearerTokenAuthenticationToken.class.isAssignableFrom(authentication); +// } + +// @Override +// public Authentication authenticate(Authentication authentication) throws AuthenticationException { +// logger.debug("Verifying key with secret '" + secret + "'"); +// BearerTokenAuthenticationToken bearer = (BearerTokenAuthenticationToken) authentication; +// try { +// SecretKeySpec secretKey = new SecretKeySpec(this.secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"); + +// Jws jws = Jwts.parserBuilder() +// .deserializeJsonWith(new JacksonDeserializer(Maps.of("profile", Profile.class).build())) +// .requireIssuer(this.issuer) +// .setSigningKey(secretKey) +// .build() +// .parseClaimsJws(bearer.getToken()); + +// return new JwtAuthenticationToken(jws); +// } catch (JwtException ex) { +// logger.error("Failed to authenticate JWT token with error", ex); +// throw new JwtAuthenticationException("Invalid JWT token", ex); +// } +// } +// } \ No newline at end of file diff --git a/storage/src/main/java/com/github/advancedsecurity/storageservice/security/JwtAuthenticationSuccessHandler.java b/storage/src/main/java/com/github/advancedsecurity/storageservice/security/JwtAuthenticationSuccessHandler.java new file mode 100644 index 0000000..755540c --- /dev/null +++ b/storage/src/main/java/com/github/advancedsecurity/storageservice/security/JwtAuthenticationSuccessHandler.java @@ -0,0 +1,21 @@ +// package com.github.advancedsecurity.storageservice.security; + +// import java.io.IOException; + +// import org.slf4j.Logger; +// import org.slf4j.LoggerFactory; +// import javax.servlet.http.HttpServletRequest; +// import javax.servlet.http.HttpServletResponse; +// import javax.servlet.ServletException; +// import org.springframework.stereotype.Component; +// import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +// import org.springframework.security.access.AccessDeniedException; +// import org.springframework.security.core.Authentication; + +// @Component +// public class JwtAuthenticationSuccessHandler implements AuthenticationSuccessHandler { +// @Override +// public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { +// // We do nothing on success since no redirection is needed. +// } +// } \ No newline at end of file diff --git a/storage/src/main/java/com/github/advancedsecurity/storageservice/security/JwtAuthenticationToken.java b/storage/src/main/java/com/github/advancedsecurity/storageservice/security/JwtAuthenticationToken.java new file mode 100644 index 0000000..5f57d72 --- /dev/null +++ b/storage/src/main/java/com/github/advancedsecurity/storageservice/security/JwtAuthenticationToken.java @@ -0,0 +1,54 @@ +// package com.github.advancedsecurity.storageservice.security; + +// import java.util.Collection; +// import java.util.ArrayList; + +// import org.springframework.security.authentication.AbstractAuthenticationToken; +// import org.springframework.security.core.AuthenticationException; +// import org.springframework.security.core.GrantedAuthority; + +// import com.github.advancedsecurity.storageservice.models.Profile; + +// import io.jsonwebtoken.Jwts; +// import io.jsonwebtoken.Jws; +// import io.jsonwebtoken.Claims; + +// public class JwtAuthenticationToken extends AbstractAuthenticationToken { +// private Claims claims; + +// public JwtAuthenticationToken(Jws jws) { +// super(new ArrayList()); +// this.claims = jws.getBody(); +// } + +// @Override +// public String getName() { +// return claims.get("profile", Profile.class).login; +// } + +// @Override +// public Object getPrincipal() { +// return claims.get("profile", Profile.class); +// } + +// @Override +// public Object getCredentials() { +// return claims.get("profile", Profile.class); +// } + +// @Override +// public Object getDetails() { +// return claims.get("profile", Profile.class); +// } + +// @Override +// public boolean isAuthenticated() { +// return true; +// } + +// @Override +// public Collection getAuthorities() { +// return new ArrayList(); +// } + +// } \ No newline at end of file diff --git a/storage/src/main/resources/.env b/storage/src/main/resources/.env new file mode 100644 index 0000000..40ecdba --- /dev/null +++ b/storage/src/main/resources/.env @@ -0,0 +1,7 @@ +AWS_ACCESS_KEY_ID="AKIAZBVE345SKPTEAHQD" +AWS_SECRET_ACCESS_KEY="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" +AWS_REGION="us-east-1" +GOOGLE_API_KEY="AIzaSyDvc2t8M5wjfDonZ1e4xG3wjFcCWHIN0yE" +MAILGUN_API_KEY="key-a67a11111111a11a1a1ba1bbcf11f1c5" +STRIPE_API_KEY="sk_live_devboxbcct1DfwS2ClCIKljW" +GOCARDLESS_API_KEY="live_AlN-kpH1H4wGhpLgwwm5kg15snC6nVfL05tHSXRB" diff --git a/storage/src/main/resources/application.properties b/storage/src/main/resources/application.properties new file mode 100644 index 0000000..8c1a533 --- /dev/null +++ b/storage/src/main/resources/application.properties @@ -0,0 +1,11 @@ +blob.allowed-content-types=image/png,image/jpeg,image/jpg +jwt.secret=secretsecret1234secretsecret1234 +jwt.issuer=OctoGallery +logging.level.com.github.advancedsecurity.storageservice=DEBUG +minio.password=mona_value_abc124 +minio.put-object-part-size=5242880 +minio.url=http://localhost:9000 +minio.username=minio +server.port=8082 +spring.security.user.name=user +spring.security.user.password=pass diff --git a/universe-utils/go.sarif b/universe-utils/go.sarif new file mode 100644 index 0000000..4cc6a9b --- /dev/null +++ b/universe-utils/go.sarif @@ -0,0 +1,4932 @@ +{ + "$schema" : "https://json.schemastore.org/sarif-2.1.0.json", + "version" : "2.1.0", + "runs" : [ { + "tool" : { + "driver" : { + "name" : "CodeQL", + "organization" : "GitHub", + "semanticVersion" : "2.15.1", + "notifications" : [ { + "id" : "cli/expected-extracted-files/java", + "name" : "cli/expected-extracted-files/java", + "shortDescription" : { + "text" : "Expected extracted files" + }, + "fullDescription" : { + "text" : "Files appearing in the source archive that are expected to be extracted." + }, + "defaultConfiguration" : { + "enabled" : true + }, + "properties" : { + "tags" : [ "expected-extracted-files", "telemetry" ], + "languageDisplayName" : "Java" + } + }, { + "id" : "cli/expected-extracted-files/go", + "name" : "cli/expected-extracted-files/go", + "shortDescription" : { + "text" : "Expected extracted files" + }, + "fullDescription" : { + "text" : "Files appearing in the source archive that are expected to be extracted." + }, + "defaultConfiguration" : { + "enabled" : true + }, + "properties" : { + "tags" : [ "expected-extracted-files", "telemetry" ], + "languageDisplayName" : "Go" + } + }, { + "id" : "cli/expected-extracted-files/javascript", + "name" : "cli/expected-extracted-files/javascript", + "shortDescription" : { + "text" : "Expected extracted files" + }, + "fullDescription" : { + "text" : "Files appearing in the source archive that are expected to be extracted." + }, + "defaultConfiguration" : { + "enabled" : true + }, + "properties" : { + "tags" : [ "expected-extracted-files", "telemetry" ], + "languageDisplayName" : "JavaScript" + } + }, { + "id" : "cli/expected-extracted-files/python", + "name" : "cli/expected-extracted-files/python", + "shortDescription" : { + "text" : "Expected extracted files" + }, + "fullDescription" : { + "text" : "Files appearing in the source archive that are expected to be extracted." + }, + "defaultConfiguration" : { + "enabled" : true + }, + "properties" : { + "tags" : [ "expected-extracted-files", "telemetry" ], + "languageDisplayName" : "Python" + } + }, { + "id" : "go/autobuilder/multiple-go-mod-found-not-nested", + "name" : "go/autobuilder/multiple-go-mod-found-not-nested", + "shortDescription" : { + "text" : "Multiple `go.mod` files found, not all nested under one root `go.mod` file" + }, + "fullDescription" : { + "text" : "Multiple `go.mod` files found, not all nested under one root `go.mod` file" + }, + "defaultConfiguration" : { + "enabled" : true + } + }, { + "id" : "go/autobuilder/package-not-found", + "name" : "go/autobuilder/package-not-found", + "shortDescription" : { + "text" : "Some packages could not be found" + }, + "fullDescription" : { + "text" : "Some packages could not be found" + }, + "defaultConfiguration" : { + "enabled" : true + } + } ], + "rules" : [ ] + }, + "extensions" : [ { + "name" : "codeql/go-queries", + "semanticVersion" : "0.7.1+8e890571ed7b21bc10698c5dbd032b9ed551d8f1", + "notifications" : [ { + "id" : "go/diagnostics/successfully-extracted-files", + "name" : "go/diagnostics/successfully-extracted-files", + "shortDescription" : { + "text" : "Successfully analyzed files" + }, + "fullDescription" : { + "text" : "List all files that were successfully extracted." + }, + "defaultConfiguration" : { + "enabled" : true + }, + "properties" : { + "tags" : [ "successfully-extracted-files" ], + "description" : "List all files that were successfully extracted.", + "id" : "go/diagnostics/successfully-extracted-files", + "kind" : "diagnostic", + "name" : "Successfully analyzed files" + } + }, { + "id" : "go/diagnostics/extraction-errors", + "name" : "go/diagnostics/extraction-errors", + "shortDescription" : { + "text" : "Extraction errors" + }, + "fullDescription" : { + "text" : "List all extraction errors for files in the source code directory." + }, + "defaultConfiguration" : { + "enabled" : true + }, + "properties" : { + "description" : "List all extraction errors for files in the source code directory.", + "id" : "go/diagnostics/extraction-errors", + "kind" : "diagnostic", + "name" : "Extraction errors" + } + } ], + "rules" : [ { + "id" : "go/incorrect-integer-conversion", + "name" : "go/incorrect-integer-conversion", + "shortDescription" : { + "text" : "Incorrect conversion between integer types" + }, + "fullDescription" : { + "text" : "Converting the result of `strconv.Atoi`, `strconv.ParseInt`, and `strconv.ParseUint` to integer types of smaller bit size can produce unexpected values." + }, + "defaultConfiguration" : { + "enabled" : true, + "level" : "warning" + }, + "help" : { + "text" : "# Incorrect conversion between integer types\nIf a string is parsed into an int using `strconv.Atoi`, and subsequently that int is converted into another integer type of a smaller size, the result can produce unexpected values.\n\nThis also applies to the results of `strconv.ParseInt` and `strconv.ParseUint` when the specified size is larger than the size of the type that number is converted to.\n\n\n## Recommendation\nIf you need to parse integer values with specific bit sizes, avoid `strconv.Atoi`, and instead use `strconv.ParseInt` or `strconv.ParseUint`, which also allow specifying the bit size.\n\nWhen using those functions, be careful to not convert the result to another type with a smaller bit size than the bit size you specified when parsing the number.\n\nIf this is not possible, then add upper (and lower) bound checks specific to each type and bit size (you can find the minimum and maximum value for each type in the `math` package).\n\n\n## Example\nIn the first example, assume that an input string is passed to `parseAllocateBad1` function, parsed by `strconv.Atoi`, and then converted into an `int32` type:\n\n\n```go\npackage main\n\nimport (\n\t\"strconv\"\n)\n\nfunc parseAllocateBad1(wanted string) int32 {\n\tparsed, err := strconv.Atoi(wanted)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn int32(parsed)\n}\nfunc parseAllocateBad2(wanted string) int32 {\n\tparsed, err := strconv.ParseInt(wanted, 10, 64)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn int32(parsed)\n}\n\n```\nThe bounds are not checked, so this means that if the provided number is greater than the maximum value of type `int32`, the resulting value from the conversion will be different from the actual provided value.\n\nTo avoid unexpected values, you should either use the other functions provided by the `strconv` package to parse the specific types and bit sizes as shown in the `parseAllocateGood2` function; or check bounds as in the `parseAllocateGood1` function.\n\n\n```go\npackage main\n\nimport (\n\t\"math\"\n\t\"strconv\"\n)\n\nfunc main() {\n\n}\n\nconst DefaultAllocate int32 = 256\n\nfunc parseAllocateGood1(desired string) int32 {\n\tparsed, err := strconv.Atoi(desired)\n\tif err != nil {\n\t\treturn DefaultAllocate\n\t}\n\t// GOOD: check for lower and upper bounds\n\tif parsed > 0 && parsed <= math.MaxInt32 {\n\t\treturn int32(parsed)\n\t}\n\treturn DefaultAllocate\n}\nfunc parseAllocateGood2(desired string) int32 {\n\t// GOOD: parse specifying the bit size\n\tparsed, err := strconv.ParseInt(desired, 10, 32)\n\tif err != nil {\n\t\treturn DefaultAllocate\n\t}\n\treturn int32(parsed)\n}\n\nfunc parseAllocateGood3(wanted string) int32 {\n\tparsed, err := strconv.ParseInt(wanted, 10, 32)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn int32(parsed)\n}\nfunc parseAllocateGood4(wanted string) int32 {\n\tparsed, err := strconv.ParseInt(wanted, 10, 64)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\t// GOOD: check for lower and uppper bounds\n\tif parsed > 0 && parsed <= math.MaxInt32 {\n\t\treturn int32(parsed)\n\t}\n\treturn DefaultAllocate\n}\n\n```\n\n## Example\nIn the second example, assume that an input string is passed to `parseAllocateBad2` function, parsed by `strconv.ParseInt` with a bit size set to 64, and then converted into an `int32` type:\n\n\n```go\npackage main\n\nimport (\n\t\"strconv\"\n)\n\nfunc parseAllocateBad1(wanted string) int32 {\n\tparsed, err := strconv.Atoi(wanted)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn int32(parsed)\n}\nfunc parseAllocateBad2(wanted string) int32 {\n\tparsed, err := strconv.ParseInt(wanted, 10, 64)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn int32(parsed)\n}\n\n```\nIf the provided number is greater than the maximum value of type `int32`, the resulting value from the conversion will be different from the actual provided value.\n\nTo avoid unexpected values, you should specify the correct bit size as in `parseAllocateGood3`; or check bounds before making the conversion as in `parseAllocateGood4`.\n\n\n```go\npackage main\n\nimport (\n\t\"math\"\n\t\"strconv\"\n)\n\nfunc main() {\n\n}\n\nconst DefaultAllocate int32 = 256\n\nfunc parseAllocateGood1(desired string) int32 {\n\tparsed, err := strconv.Atoi(desired)\n\tif err != nil {\n\t\treturn DefaultAllocate\n\t}\n\t// GOOD: check for lower and upper bounds\n\tif parsed > 0 && parsed <= math.MaxInt32 {\n\t\treturn int32(parsed)\n\t}\n\treturn DefaultAllocate\n}\nfunc parseAllocateGood2(desired string) int32 {\n\t// GOOD: parse specifying the bit size\n\tparsed, err := strconv.ParseInt(desired, 10, 32)\n\tif err != nil {\n\t\treturn DefaultAllocate\n\t}\n\treturn int32(parsed)\n}\n\nfunc parseAllocateGood3(wanted string) int32 {\n\tparsed, err := strconv.ParseInt(wanted, 10, 32)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn int32(parsed)\n}\nfunc parseAllocateGood4(wanted string) int32 {\n\tparsed, err := strconv.ParseInt(wanted, 10, 64)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\t// GOOD: check for lower and uppper bounds\n\tif parsed > 0 && parsed <= math.MaxInt32 {\n\t\treturn int32(parsed)\n\t}\n\treturn DefaultAllocate\n}\n\n```\n\n## References\n* Wikipedia [Integer overflow](https://en.wikipedia.org/wiki/Integer_overflow).\n* Go language specification [Integer overflow](https://golang.org/ref/spec#Integer_overflow).\n* Documentation for [strconv.Atoi](https://golang.org/pkg/strconv/#Atoi).\n* Documentation for [strconv.ParseInt](https://golang.org/pkg/strconv/#ParseInt).\n* Documentation for [strconv.ParseUint](https://golang.org/pkg/strconv/#ParseUint).\n* Common Weakness Enumeration: [CWE-190](https://cwe.mitre.org/data/definitions/190.html).\n* Common Weakness Enumeration: [CWE-681](https://cwe.mitre.org/data/definitions/681.html).\n", + "markdown" : "# Incorrect conversion between integer types\nIf a string is parsed into an int using `strconv.Atoi`, and subsequently that int is converted into another integer type of a smaller size, the result can produce unexpected values.\n\nThis also applies to the results of `strconv.ParseInt` and `strconv.ParseUint` when the specified size is larger than the size of the type that number is converted to.\n\n\n## Recommendation\nIf you need to parse integer values with specific bit sizes, avoid `strconv.Atoi`, and instead use `strconv.ParseInt` or `strconv.ParseUint`, which also allow specifying the bit size.\n\nWhen using those functions, be careful to not convert the result to another type with a smaller bit size than the bit size you specified when parsing the number.\n\nIf this is not possible, then add upper (and lower) bound checks specific to each type and bit size (you can find the minimum and maximum value for each type in the `math` package).\n\n\n## Example\nIn the first example, assume that an input string is passed to `parseAllocateBad1` function, parsed by `strconv.Atoi`, and then converted into an `int32` type:\n\n\n```go\npackage main\n\nimport (\n\t\"strconv\"\n)\n\nfunc parseAllocateBad1(wanted string) int32 {\n\tparsed, err := strconv.Atoi(wanted)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn int32(parsed)\n}\nfunc parseAllocateBad2(wanted string) int32 {\n\tparsed, err := strconv.ParseInt(wanted, 10, 64)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn int32(parsed)\n}\n\n```\nThe bounds are not checked, so this means that if the provided number is greater than the maximum value of type `int32`, the resulting value from the conversion will be different from the actual provided value.\n\nTo avoid unexpected values, you should either use the other functions provided by the `strconv` package to parse the specific types and bit sizes as shown in the `parseAllocateGood2` function; or check bounds as in the `parseAllocateGood1` function.\n\n\n```go\npackage main\n\nimport (\n\t\"math\"\n\t\"strconv\"\n)\n\nfunc main() {\n\n}\n\nconst DefaultAllocate int32 = 256\n\nfunc parseAllocateGood1(desired string) int32 {\n\tparsed, err := strconv.Atoi(desired)\n\tif err != nil {\n\t\treturn DefaultAllocate\n\t}\n\t// GOOD: check for lower and upper bounds\n\tif parsed > 0 && parsed <= math.MaxInt32 {\n\t\treturn int32(parsed)\n\t}\n\treturn DefaultAllocate\n}\nfunc parseAllocateGood2(desired string) int32 {\n\t// GOOD: parse specifying the bit size\n\tparsed, err := strconv.ParseInt(desired, 10, 32)\n\tif err != nil {\n\t\treturn DefaultAllocate\n\t}\n\treturn int32(parsed)\n}\n\nfunc parseAllocateGood3(wanted string) int32 {\n\tparsed, err := strconv.ParseInt(wanted, 10, 32)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn int32(parsed)\n}\nfunc parseAllocateGood4(wanted string) int32 {\n\tparsed, err := strconv.ParseInt(wanted, 10, 64)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\t// GOOD: check for lower and uppper bounds\n\tif parsed > 0 && parsed <= math.MaxInt32 {\n\t\treturn int32(parsed)\n\t}\n\treturn DefaultAllocate\n}\n\n```\n\n## Example\nIn the second example, assume that an input string is passed to `parseAllocateBad2` function, parsed by `strconv.ParseInt` with a bit size set to 64, and then converted into an `int32` type:\n\n\n```go\npackage main\n\nimport (\n\t\"strconv\"\n)\n\nfunc parseAllocateBad1(wanted string) int32 {\n\tparsed, err := strconv.Atoi(wanted)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn int32(parsed)\n}\nfunc parseAllocateBad2(wanted string) int32 {\n\tparsed, err := strconv.ParseInt(wanted, 10, 64)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn int32(parsed)\n}\n\n```\nIf the provided number is greater than the maximum value of type `int32`, the resulting value from the conversion will be different from the actual provided value.\n\nTo avoid unexpected values, you should specify the correct bit size as in `parseAllocateGood3`; or check bounds before making the conversion as in `parseAllocateGood4`.\n\n\n```go\npackage main\n\nimport (\n\t\"math\"\n\t\"strconv\"\n)\n\nfunc main() {\n\n}\n\nconst DefaultAllocate int32 = 256\n\nfunc parseAllocateGood1(desired string) int32 {\n\tparsed, err := strconv.Atoi(desired)\n\tif err != nil {\n\t\treturn DefaultAllocate\n\t}\n\t// GOOD: check for lower and upper bounds\n\tif parsed > 0 && parsed <= math.MaxInt32 {\n\t\treturn int32(parsed)\n\t}\n\treturn DefaultAllocate\n}\nfunc parseAllocateGood2(desired string) int32 {\n\t// GOOD: parse specifying the bit size\n\tparsed, err := strconv.ParseInt(desired, 10, 32)\n\tif err != nil {\n\t\treturn DefaultAllocate\n\t}\n\treturn int32(parsed)\n}\n\nfunc parseAllocateGood3(wanted string) int32 {\n\tparsed, err := strconv.ParseInt(wanted, 10, 32)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\treturn int32(parsed)\n}\nfunc parseAllocateGood4(wanted string) int32 {\n\tparsed, err := strconv.ParseInt(wanted, 10, 64)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\t// GOOD: check for lower and uppper bounds\n\tif parsed > 0 && parsed <= math.MaxInt32 {\n\t\treturn int32(parsed)\n\t}\n\treturn DefaultAllocate\n}\n\n```\n\n## References\n* Wikipedia [Integer overflow](https://en.wikipedia.org/wiki/Integer_overflow).\n* Go language specification [Integer overflow](https://golang.org/ref/spec#Integer_overflow).\n* Documentation for [strconv.Atoi](https://golang.org/pkg/strconv/#Atoi).\n* Documentation for [strconv.ParseInt](https://golang.org/pkg/strconv/#ParseInt).\n* Documentation for [strconv.ParseUint](https://golang.org/pkg/strconv/#ParseUint).\n* Common Weakness Enumeration: [CWE-190](https://cwe.mitre.org/data/definitions/190.html).\n* Common Weakness Enumeration: [CWE-681](https://cwe.mitre.org/data/definitions/681.html).\n" + }, + "properties" : { + "tags" : [ "security", "external/cwe/cwe-190", "external/cwe/cwe-681" ], + "description" : "Converting the result of `strconv.Atoi`, `strconv.ParseInt`,\n and `strconv.ParseUint` to integer types of smaller bit size\n can produce unexpected values.", + "id" : "go/incorrect-integer-conversion", + "kind" : "path-problem", + "name" : "Incorrect conversion between integer types", + "precision" : "very-high", + "problem.severity" : "warning", + "security-severity" : "8.1" + } + }, { + "id" : "go/constant-oauth2-state", + "name" : "go/constant-oauth2-state", + "shortDescription" : { + "text" : "Use of constant `state` value in OAuth 2.0 URL" + }, + "fullDescription" : { + "text" : "Using a constant value for the `state` in the OAuth 2.0 URL makes the application susceptible to CSRF attacks." + }, + "defaultConfiguration" : { + "enabled" : true, + "level" : "error" + }, + "help" : { + "text" : "# Use of constant `state` value in OAuth 2.0 URL\nOAuth 2.0 clients must implement CSRF protection for the redirection URI, which is typically accomplished by including a \"state\" value that binds the request to the user's authenticated state. The Go OAuth 2.0 library allows you to specify a \"state\" value which is then included in the auth code URL. That state is then provided back by the remote authentication server in the redirect callback, from where it must be validated. Failure to do so makes the client susceptible to an CSRF attack.\n\n\n## Recommendation\nAlways include a unique, non-guessable `state` value (provided to the call to `AuthCodeURL` function) that is also bound to the user's authenticated state with each authentication request, and then validated in the redirect callback.\n\n\n## Example\nThe first example shows you the use of a constant state (bad).\n\n\n```go\npackage main\n\nimport (\n\t\"golang.org/x/oauth2\"\n)\n\nfunc main() {}\n\nvar stateStringVar = \"state\"\n\nfunc badWithStringLiteralState() {\n\tconf := &oauth2.Config{\n\t\tClientID: \"YOUR_CLIENT_ID\",\n\t\tClientSecret: \"YOUR_CLIENT_SECRET\",\n\t\tScopes: []string{\"SCOPE1\", \"SCOPE2\"},\n\t\tEndpoint: oauth2.Endpoint{\n\t\t\tAuthURL: \"https://provider.com/o/oauth2/auth\",\n\t\t\tTokenURL: \"https://provider.com/o/oauth2/token\",\n\t\t},\n\t}\n\n\turl := conf.AuthCodeURL(stateStringVar)\n\t// ...\n}\n\n```\nThe second example shows a better implementation idea.\n\n\n```go\npackage main\n\nimport (\n\t\"crypto/rand\"\n\t\"encoding/base64\"\n\t\"net/http\"\n\n\t\"golang.org/x/oauth2\"\n)\n\nfunc betterWithVariableStateReturned(w http.ResponseWriter) {\n\tconf := &oauth2.Config{\n\t\tClientID: \"YOUR_CLIENT_ID\",\n\t\tClientSecret: \"YOUR_CLIENT_SECRET\",\n\t\tScopes: []string{\"SCOPE1\", \"SCOPE2\"},\n\t\tEndpoint: oauth2.Endpoint{\n\t\t\tAuthURL: \"https://provider.com/o/oauth2/auth\",\n\t\t\tTokenURL: \"https://provider.com/o/oauth2/token\",\n\t\t},\n\t}\n\n\tstate := generateStateOauthCookie(w)\n\turl := conf.AuthCodeURL(state)\n\t_ = url\n\t// ...\n}\nfunc generateStateOauthCookie(w http.ResponseWriter) string {\n\tb := make([]byte, 128)\n\trand.Read(b)\n\t// TODO: save the state string to cookies or HTML storage,\n\t// and bind it to the authenticated status of the user.\n\tstate := base64.URLEncoding.EncodeToString(b)\n\n\treturn state\n}\n\n```\n\n## References\n* IETF: [The OAuth 2.0 Authorization Framework](https://tools.ietf.org/html/rfc6749#section-10.12)\n* IETF: [OAuth 2.0 Security Best Current Practice](https://tools.ietf.org/html/draft-ietf-oauth-security-topics-15#section-2.1)\n* Common Weakness Enumeration: [CWE-352](https://cwe.mitre.org/data/definitions/352.html).\n", + "markdown" : "# Use of constant `state` value in OAuth 2.0 URL\nOAuth 2.0 clients must implement CSRF protection for the redirection URI, which is typically accomplished by including a \"state\" value that binds the request to the user's authenticated state. The Go OAuth 2.0 library allows you to specify a \"state\" value which is then included in the auth code URL. That state is then provided back by the remote authentication server in the redirect callback, from where it must be validated. Failure to do so makes the client susceptible to an CSRF attack.\n\n\n## Recommendation\nAlways include a unique, non-guessable `state` value (provided to the call to `AuthCodeURL` function) that is also bound to the user's authenticated state with each authentication request, and then validated in the redirect callback.\n\n\n## Example\nThe first example shows you the use of a constant state (bad).\n\n\n```go\npackage main\n\nimport (\n\t\"golang.org/x/oauth2\"\n)\n\nfunc main() {}\n\nvar stateStringVar = \"state\"\n\nfunc badWithStringLiteralState() {\n\tconf := &oauth2.Config{\n\t\tClientID: \"YOUR_CLIENT_ID\",\n\t\tClientSecret: \"YOUR_CLIENT_SECRET\",\n\t\tScopes: []string{\"SCOPE1\", \"SCOPE2\"},\n\t\tEndpoint: oauth2.Endpoint{\n\t\t\tAuthURL: \"https://provider.com/o/oauth2/auth\",\n\t\t\tTokenURL: \"https://provider.com/o/oauth2/token\",\n\t\t},\n\t}\n\n\turl := conf.AuthCodeURL(stateStringVar)\n\t// ...\n}\n\n```\nThe second example shows a better implementation idea.\n\n\n```go\npackage main\n\nimport (\n\t\"crypto/rand\"\n\t\"encoding/base64\"\n\t\"net/http\"\n\n\t\"golang.org/x/oauth2\"\n)\n\nfunc betterWithVariableStateReturned(w http.ResponseWriter) {\n\tconf := &oauth2.Config{\n\t\tClientID: \"YOUR_CLIENT_ID\",\n\t\tClientSecret: \"YOUR_CLIENT_SECRET\",\n\t\tScopes: []string{\"SCOPE1\", \"SCOPE2\"},\n\t\tEndpoint: oauth2.Endpoint{\n\t\t\tAuthURL: \"https://provider.com/o/oauth2/auth\",\n\t\t\tTokenURL: \"https://provider.com/o/oauth2/token\",\n\t\t},\n\t}\n\n\tstate := generateStateOauthCookie(w)\n\turl := conf.AuthCodeURL(state)\n\t_ = url\n\t// ...\n}\nfunc generateStateOauthCookie(w http.ResponseWriter) string {\n\tb := make([]byte, 128)\n\trand.Read(b)\n\t// TODO: save the state string to cookies or HTML storage,\n\t// and bind it to the authenticated status of the user.\n\tstate := base64.URLEncoding.EncodeToString(b)\n\n\treturn state\n}\n\n```\n\n## References\n* IETF: [The OAuth 2.0 Authorization Framework](https://tools.ietf.org/html/rfc6749#section-10.12)\n* IETF: [OAuth 2.0 Security Best Current Practice](https://tools.ietf.org/html/draft-ietf-oauth-security-topics-15#section-2.1)\n* Common Weakness Enumeration: [CWE-352](https://cwe.mitre.org/data/definitions/352.html).\n" + }, + "properties" : { + "tags" : [ "security", "external/cwe/cwe-352" ], + "description" : "Using a constant value for the `state` in the OAuth 2.0 URL makes the application\n susceptible to CSRF attacks.", + "id" : "go/constant-oauth2-state", + "kind" : "path-problem", + "name" : "Use of constant `state` value in OAuth 2.0 URL", + "precision" : "high", + "problem.severity" : "error", + "security-severity" : "8.8" + } + }, { + "id" : "go/disabled-certificate-check", + "name" : "go/disabled-certificate-check", + "shortDescription" : { + "text" : "Disabled TLS certificate check" + }, + "fullDescription" : { + "text" : "If an application disables TLS certificate checking, it may be vulnerable to man-in-the-middle attacks." + }, + "defaultConfiguration" : { + "enabled" : true, + "level" : "warning" + }, + "help" : { + "text" : "# Disabled TLS certificate check\nThe field `InsecureSkipVerify` controls whether a TLS client verifies the server's certificate chain and host name. If set to `true`, the client will accept any certificate and any host name in that certificate, making it susceptible to man-in-the-middle attacks.\n\n\n## Recommendation\nDo not set `InsecureSkipVerify` to `true` except in tests.\n\n\n## Example\nThe following code snippet shows a function that performs an HTTP request over TLS with certificate verification disabled:\n\n\n```go\npackage main\n\nimport (\n\t\"crypto/tls\"\n\t\"net/http\"\n)\n\nfunc doAuthReq(authReq *http.Request) *http.Response {\n\ttr := &http.Transport{\n\t\tTLSClientConfig: &tls.Config{InsecureSkipVerify: true},\n\t}\n\tclient := &http.Client{Transport: tr}\n\tres, _ := client.Do(authReq)\n\treturn res\n}\n\n```\nWhile this is acceptable in a test, it should not be used in production code. Instead, certificates should be configured such that verification can be performed.\n\n\n## References\n* Package tls: [Config](https://golang.org/pkg/crypto/tls/#Config).\n* SSL.com: [Browsers and Certificate Validation](https://www.ssl.com/article/browsers-and-certificate-validation/).\n* Common Weakness Enumeration: [CWE-295](https://cwe.mitre.org/data/definitions/295.html).\n", + "markdown" : "# Disabled TLS certificate check\nThe field `InsecureSkipVerify` controls whether a TLS client verifies the server's certificate chain and host name. If set to `true`, the client will accept any certificate and any host name in that certificate, making it susceptible to man-in-the-middle attacks.\n\n\n## Recommendation\nDo not set `InsecureSkipVerify` to `true` except in tests.\n\n\n## Example\nThe following code snippet shows a function that performs an HTTP request over TLS with certificate verification disabled:\n\n\n```go\npackage main\n\nimport (\n\t\"crypto/tls\"\n\t\"net/http\"\n)\n\nfunc doAuthReq(authReq *http.Request) *http.Response {\n\ttr := &http.Transport{\n\t\tTLSClientConfig: &tls.Config{InsecureSkipVerify: true},\n\t}\n\tclient := &http.Client{Transport: tr}\n\tres, _ := client.Do(authReq)\n\treturn res\n}\n\n```\nWhile this is acceptable in a test, it should not be used in production code. Instead, certificates should be configured such that verification can be performed.\n\n\n## References\n* Package tls: [Config](https://golang.org/pkg/crypto/tls/#Config).\n* SSL.com: [Browsers and Certificate Validation](https://www.ssl.com/article/browsers-and-certificate-validation/).\n* Common Weakness Enumeration: [CWE-295](https://cwe.mitre.org/data/definitions/295.html).\n" + }, + "properties" : { + "tags" : [ "security", "external/cwe/cwe-295" ], + "description" : "If an application disables TLS certificate checking, it may be vulnerable to\n man-in-the-middle attacks.", + "id" : "go/disabled-certificate-check", + "kind" : "problem", + "name" : "Disabled TLS certificate check", + "precision" : "high", + "problem.severity" : "warning", + "security-severity" : "7.5" + } + }, { + "id" : "go/bad-redirect-check", + "name" : "go/bad-redirect-check", + "shortDescription" : { + "text" : "Bad redirect check" + }, + "fullDescription" : { + "text" : "A redirect check that checks for a leading slash but not two leading slashes or a leading slash followed by a backslash is incomplete." + }, + "defaultConfiguration" : { + "enabled" : true, + "level" : "error" + }, + "help" : { + "text" : "# Bad redirect check\nRedirect URLs should be checked to ensure that user input cannot cause a site to redirect to arbitrary domains. This is often done with a check that the redirect URL begins with a slash, which most of the time is an absolute redirect on the same host. However, browsers interpret URLs beginning with `//` or `/\\` as absolute URLs. For example, a redirect to `//example.com` will redirect to `https://example.com`. Thus, redirect checks must also check the second character of redirect URLs.\n\n\n## Recommendation\nAlso disallow redirect URLs starting with `//` or `/\\`.\n\n\n## Example\nThe following function validates a (presumably untrusted) redirect URL `redir`. If it does not begin with `/`, the harmless placeholder redirect URL `/` is returned to prevent an open redirect; otherwise `redir` itself is returned.\n\n\n```go\npackage main\n\nfunc sanitizeUrl(redir string) string {\n\tif len(redir) > 0 && redir[0] == '/' {\n\t\treturn redir\n\t}\n\treturn \"/\"\n}\n\n```\nWhile this check provides partial protection, it should be extended to cover `//` and `/\\` as well:\n\n\n```go\npackage main\n\nfunc sanitizeUrl1(redir string) string {\n\tif len(redir) > 1 && redir[0] == '/' && redir[1] != '/' && redir[1] != '\\\\' {\n\t\treturn redir\n\t}\n\treturn \"/\"\n}\n\n```\n\n## References\n* OWASP: [ XSS Unvalidated Redirects and Forwards Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html#validating-urls).\n* Common Weakness Enumeration: [CWE-601](https://cwe.mitre.org/data/definitions/601.html).\n", + "markdown" : "# Bad redirect check\nRedirect URLs should be checked to ensure that user input cannot cause a site to redirect to arbitrary domains. This is often done with a check that the redirect URL begins with a slash, which most of the time is an absolute redirect on the same host. However, browsers interpret URLs beginning with `//` or `/\\` as absolute URLs. For example, a redirect to `//example.com` will redirect to `https://example.com`. Thus, redirect checks must also check the second character of redirect URLs.\n\n\n## Recommendation\nAlso disallow redirect URLs starting with `//` or `/\\`.\n\n\n## Example\nThe following function validates a (presumably untrusted) redirect URL `redir`. If it does not begin with `/`, the harmless placeholder redirect URL `/` is returned to prevent an open redirect; otherwise `redir` itself is returned.\n\n\n```go\npackage main\n\nfunc sanitizeUrl(redir string) string {\n\tif len(redir) > 0 && redir[0] == '/' {\n\t\treturn redir\n\t}\n\treturn \"/\"\n}\n\n```\nWhile this check provides partial protection, it should be extended to cover `//` and `/\\` as well:\n\n\n```go\npackage main\n\nfunc sanitizeUrl1(redir string) string {\n\tif len(redir) > 1 && redir[0] == '/' && redir[1] != '/' && redir[1] != '\\\\' {\n\t\treturn redir\n\t}\n\treturn \"/\"\n}\n\n```\n\n## References\n* OWASP: [ XSS Unvalidated Redirects and Forwards Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html#validating-urls).\n* Common Weakness Enumeration: [CWE-601](https://cwe.mitre.org/data/definitions/601.html).\n" + }, + "properties" : { + "tags" : [ "security", "external/cwe/cwe-601" ], + "description" : "A redirect check that checks for a leading slash but not two\n leading slashes or a leading slash followed by a backslash is\n incomplete.", + "id" : "go/bad-redirect-check", + "kind" : "path-problem", + "name" : "Bad redirect check", + "precision" : "high", + "problem.severity" : "error", + "security-severity" : "6.1" + } + }, { + "id" : "go/unvalidated-url-redirection", + "name" : "go/unvalidated-url-redirection", + "shortDescription" : { + "text" : "Open URL redirect" + }, + "fullDescription" : { + "text" : "Open URL redirection based on unvalidated user input may cause redirection to malicious web sites." + }, + "defaultConfiguration" : { + "enabled" : true, + "level" : "warning" + }, + "help" : { + "text" : "# Open URL redirect\nDirectly incorporating user input into a URL redirect request without validating the input can facilitate phishing attacks. In these attacks, unsuspecting users can be redirected to a malicious site that looks very similar to the real site they intend to visit, but is controlled by the attacker.\n\n\n## Recommendation\nTo guard against untrusted URL redirection, it is advisable to avoid putting user input directly into a redirect URL. Instead, maintain a list of authorized redirects on the server; then choose from that list based on the user input provided.\n\n\n## Example\nThe following example shows an HTTP request parameter being used directly in a URL redirect without validating the input, which facilitates phishing attacks:\n\n\n```go\npackage main\n\nimport (\n\t\"net/http\"\n)\n\nfunc serve() {\n\thttp.HandleFunc(\"/redir\", func(w http.ResponseWriter, r *http.Request) {\n\t\tr.ParseForm()\n\t\thttp.Redirect(w, r, r.Form.Get(\"target\"), 302)\n\t})\n}\n\n```\nOne way to remedy the problem is to validate the user input against a known fixed string before doing the redirection:\n\n\n```go\npackage main\n\nimport (\n\t\"net/http\"\n\t\"net/url\"\n)\n\nfunc serve() {\n\thttp.HandleFunc(\"/redir\", func(w http.ResponseWriter, r *http.Request) {\n\t\tr.ParseForm()\n\t\ttarget, err := url.Parse(r.Form.Get(\"target\"))\n\t\tif err != nil {\n\t\t\t// ...\n\t\t}\n\n\t\tif target.Hostname() == \"semmle.com\" {\n\t\t\t// GOOD: checking hostname\n\t\t\thttp.Redirect(w, r, target.String(), 302)\n\t\t} else {\n\t\t\thttp.WriteHeader(400)\n\t\t}\n\t})\n}\n\n```\n\n## References\n* OWASP: [ XSS Unvalidated Redirects and Forwards Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html).\n* Common Weakness Enumeration: [CWE-601](https://cwe.mitre.org/data/definitions/601.html).\n", + "markdown" : "# Open URL redirect\nDirectly incorporating user input into a URL redirect request without validating the input can facilitate phishing attacks. In these attacks, unsuspecting users can be redirected to a malicious site that looks very similar to the real site they intend to visit, but is controlled by the attacker.\n\n\n## Recommendation\nTo guard against untrusted URL redirection, it is advisable to avoid putting user input directly into a redirect URL. Instead, maintain a list of authorized redirects on the server; then choose from that list based on the user input provided.\n\n\n## Example\nThe following example shows an HTTP request parameter being used directly in a URL redirect without validating the input, which facilitates phishing attacks:\n\n\n```go\npackage main\n\nimport (\n\t\"net/http\"\n)\n\nfunc serve() {\n\thttp.HandleFunc(\"/redir\", func(w http.ResponseWriter, r *http.Request) {\n\t\tr.ParseForm()\n\t\thttp.Redirect(w, r, r.Form.Get(\"target\"), 302)\n\t})\n}\n\n```\nOne way to remedy the problem is to validate the user input against a known fixed string before doing the redirection:\n\n\n```go\npackage main\n\nimport (\n\t\"net/http\"\n\t\"net/url\"\n)\n\nfunc serve() {\n\thttp.HandleFunc(\"/redir\", func(w http.ResponseWriter, r *http.Request) {\n\t\tr.ParseForm()\n\t\ttarget, err := url.Parse(r.Form.Get(\"target\"))\n\t\tif err != nil {\n\t\t\t// ...\n\t\t}\n\n\t\tif target.Hostname() == \"semmle.com\" {\n\t\t\t// GOOD: checking hostname\n\t\t\thttp.Redirect(w, r, target.String(), 302)\n\t\t} else {\n\t\t\thttp.WriteHeader(400)\n\t\t}\n\t})\n}\n\n```\n\n## References\n* OWASP: [ XSS Unvalidated Redirects and Forwards Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html).\n* Common Weakness Enumeration: [CWE-601](https://cwe.mitre.org/data/definitions/601.html).\n" + }, + "properties" : { + "tags" : [ "security", "external/cwe/cwe-601" ], + "description" : "Open URL redirection based on unvalidated user input\n may cause redirection to malicious web sites.", + "id" : "go/unvalidated-url-redirection", + "kind" : "path-problem", + "name" : "Open URL redirect", + "precision" : "high", + "problem.severity" : "warning", + "security-severity" : "6.1" + } + }, { + "id" : "go/reflected-xss", + "name" : "go/reflected-xss", + "shortDescription" : { + "text" : "Reflected cross-site scripting" + }, + "fullDescription" : { + "text" : "Writing user input directly to an HTTP response allows for a cross-site scripting vulnerability." + }, + "defaultConfiguration" : { + "enabled" : true, + "level" : "error" + }, + "help" : { + "text" : "# Reflected cross-site scripting\nDirectly writing user input (for example, an HTTP request parameter) to an HTTP response without properly sanitizing the input first, allows for a cross-site scripting vulnerability.\n\nThis kind of vulnerability is also called *reflected* cross-site scripting, to distinguish it from other types of cross-site scripting.\n\n\n## Recommendation\nTo guard against cross-site scripting, consider using contextual output encoding/escaping before writing user input to the response, or one of the other solutions that are mentioned in the references.\n\n\n## Example\nThe following example code writes part of an HTTP request (which is controlled by the user) directly to the response. This leaves the website vulnerable to cross-site scripting.\n\n\n```go\npackage main\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n)\n\nfunc serve() {\n\thttp.HandleFunc(\"/user\", func(w http.ResponseWriter, r *http.Request) {\n\t\tr.ParseForm()\n\t\tusername := r.Form.Get(\"username\")\n\t\tif !isValidUsername(username) {\n\t\t\t// BAD: a request parameter is incorporated without validation into the response\n\t\t\tfmt.Fprintf(w, \"%q is an unknown user\", username)\n\t\t} else {\n\t\t\t// TODO: Handle successful login\n\t\t}\n\t})\n\thttp.ListenAndServe(\":80\", nil)\n}\n\n```\nSanitizing the user-controlled data prevents the vulnerability:\n\n\n```go\npackage main\n\nimport (\n\t\"fmt\"\n\t\"html\"\n\t\"net/http\"\n)\n\nfunc serve1() {\n\thttp.HandleFunc(\"/user\", func(w http.ResponseWriter, r *http.Request) {\n\t\tr.ParseForm()\n\t\tusername := r.Form.Get(\"username\")\n\t\tif !isValidUsername(username) {\n\t\t\t// GOOD: a request parameter is escaped before being put into the response\n\t\t\tfmt.Fprintf(w, \"%q is an unknown user\", html.EscapeString(username))\n\t\t} else {\n\t\t\t// TODO: do something exciting\n\t\t}\n\t})\n\thttp.ListenAndServe(\":80\", nil)\n}\n\n```\n\n## References\n* OWASP: [XSS (Cross Site Scripting) Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html).\n* OWASP [Types of Cross-Site Scripting](https://www.owasp.org/index.php/Types_of_Cross-Site_Scripting).\n* Wikipedia: [Cross-site scripting](http://en.wikipedia.org/wiki/Cross-site_scripting).\n* Common Weakness Enumeration: [CWE-79](https://cwe.mitre.org/data/definitions/79.html).\n* Common Weakness Enumeration: [CWE-116](https://cwe.mitre.org/data/definitions/116.html).\n", + "markdown" : "# Reflected cross-site scripting\nDirectly writing user input (for example, an HTTP request parameter) to an HTTP response without properly sanitizing the input first, allows for a cross-site scripting vulnerability.\n\nThis kind of vulnerability is also called *reflected* cross-site scripting, to distinguish it from other types of cross-site scripting.\n\n\n## Recommendation\nTo guard against cross-site scripting, consider using contextual output encoding/escaping before writing user input to the response, or one of the other solutions that are mentioned in the references.\n\n\n## Example\nThe following example code writes part of an HTTP request (which is controlled by the user) directly to the response. This leaves the website vulnerable to cross-site scripting.\n\n\n```go\npackage main\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n)\n\nfunc serve() {\n\thttp.HandleFunc(\"/user\", func(w http.ResponseWriter, r *http.Request) {\n\t\tr.ParseForm()\n\t\tusername := r.Form.Get(\"username\")\n\t\tif !isValidUsername(username) {\n\t\t\t// BAD: a request parameter is incorporated without validation into the response\n\t\t\tfmt.Fprintf(w, \"%q is an unknown user\", username)\n\t\t} else {\n\t\t\t// TODO: Handle successful login\n\t\t}\n\t})\n\thttp.ListenAndServe(\":80\", nil)\n}\n\n```\nSanitizing the user-controlled data prevents the vulnerability:\n\n\n```go\npackage main\n\nimport (\n\t\"fmt\"\n\t\"html\"\n\t\"net/http\"\n)\n\nfunc serve1() {\n\thttp.HandleFunc(\"/user\", func(w http.ResponseWriter, r *http.Request) {\n\t\tr.ParseForm()\n\t\tusername := r.Form.Get(\"username\")\n\t\tif !isValidUsername(username) {\n\t\t\t// GOOD: a request parameter is escaped before being put into the response\n\t\t\tfmt.Fprintf(w, \"%q is an unknown user\", html.EscapeString(username))\n\t\t} else {\n\t\t\t// TODO: do something exciting\n\t\t}\n\t})\n\thttp.ListenAndServe(\":80\", nil)\n}\n\n```\n\n## References\n* OWASP: [XSS (Cross Site Scripting) Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html).\n* OWASP [Types of Cross-Site Scripting](https://www.owasp.org/index.php/Types_of_Cross-Site_Scripting).\n* Wikipedia: [Cross-site scripting](http://en.wikipedia.org/wiki/Cross-site_scripting).\n* Common Weakness Enumeration: [CWE-79](https://cwe.mitre.org/data/definitions/79.html).\n* Common Weakness Enumeration: [CWE-116](https://cwe.mitre.org/data/definitions/116.html).\n" + }, + "properties" : { + "tags" : [ "security", "external/cwe/cwe-079", "external/cwe/cwe-116" ], + "description" : "Writing user input directly to an HTTP response allows for\n a cross-site scripting vulnerability.", + "id" : "go/reflected-xss", + "kind" : "path-problem", + "name" : "Reflected cross-site scripting", + "precision" : "high", + "problem.severity" : "error", + "security-severity" : "6.1" + } + }, { + "id" : "go/xml/xpath-injection", + "name" : "go/xml/xpath-injection", + "shortDescription" : { + "text" : "XPath injection" + }, + "fullDescription" : { + "text" : "Building an XPath expression from user-controlled sources is vulnerable to insertion of malicious code by the user." + }, + "defaultConfiguration" : { + "enabled" : true, + "level" : "error" + }, + "help" : { + "text" : "# XPath injection\nIf an XPath expression is built using string concatenation, and the components of the concatenation include user input, a user is likely to be able to create a malicious XPath expression.\n\n\n## Recommendation\nIf user input must be included in an XPath expression, pre-compile the query and use variable references to include the user input.\n\nFor example, when using the `github.com/ChrisTrenkamp/goxpath` API, you can do this by creating a function that takes an `*goxpath.Opts` structure. In this structure you can then set the values of the variable references. This function can then be specified when calling `Exec()`, `Exec{Bool|Num|Node}()`, `ParseExec()`, or `MustExec()`.\n\n\n## Example\nIn the first example, the code accepts a username specified by the user, and uses this unvalidated and unsanitized value in an XPath expression. This is vulnerable to the user providing special characters or string sequences that change the meaning of the XPath expression to search for different values.\n\nIn the second example, the XPath expression is a hard-coded string that specifies some variables, which are safely resolved at runtime using the `goxpath.Opts` structure.\n\n\n```go\npackage main\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/ChrisTrenkamp/goxpath\"\n\t\"github.com/ChrisTrenkamp/goxpath/tree\"\n)\n\nfunc main() {}\n\nfunc processRequest(r *http.Request, doc tree.Node) {\n\tr.ParseForm()\n\tusername := r.Form.Get(\"username\")\n\n\t// BAD: User input used directly in an XPath expression\n\txPath := goxpath.MustParse(\"//users/user[login/text()='\" + username + \"']/home_dir/text()\")\n\tunsafeRes, _ := xPath.ExecBool(doc)\n\tfmt.Println(unsafeRes)\n\n\t// GOOD: Value of parameters is defined here instead of directly in the query\n\topt := func(o *goxpath.Opts) {\n\t\to.Vars[\"username\"] = tree.String(username)\n\t}\n\t// GOOD: Uses parameters to avoid including user input directly in XPath expression\n\txPath = goxpath.MustParse(\"//users/user[login/text()=$username]/home_dir/text()\")\n\tsafeRes, _ := xPath.ExecBool(doc, opt)\n\tfmt.Println(safeRes)\n}\n\n```\n\n## References\n* OWASP: [Testing for XPath Injection](https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/07-Input_Validation_Testing/09-Testing_for_XPath_Injection).\n* OWASP: [XPath Injection](https://www.owasp.org/index.php/XPATH_Injection).\n* Common Weakness Enumeration: [CWE-643](https://cwe.mitre.org/data/definitions/643.html).\n", + "markdown" : "# XPath injection\nIf an XPath expression is built using string concatenation, and the components of the concatenation include user input, a user is likely to be able to create a malicious XPath expression.\n\n\n## Recommendation\nIf user input must be included in an XPath expression, pre-compile the query and use variable references to include the user input.\n\nFor example, when using the `github.com/ChrisTrenkamp/goxpath` API, you can do this by creating a function that takes an `*goxpath.Opts` structure. In this structure you can then set the values of the variable references. This function can then be specified when calling `Exec()`, `Exec{Bool|Num|Node}()`, `ParseExec()`, or `MustExec()`.\n\n\n## Example\nIn the first example, the code accepts a username specified by the user, and uses this unvalidated and unsanitized value in an XPath expression. This is vulnerable to the user providing special characters or string sequences that change the meaning of the XPath expression to search for different values.\n\nIn the second example, the XPath expression is a hard-coded string that specifies some variables, which are safely resolved at runtime using the `goxpath.Opts` structure.\n\n\n```go\npackage main\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/ChrisTrenkamp/goxpath\"\n\t\"github.com/ChrisTrenkamp/goxpath/tree\"\n)\n\nfunc main() {}\n\nfunc processRequest(r *http.Request, doc tree.Node) {\n\tr.ParseForm()\n\tusername := r.Form.Get(\"username\")\n\n\t// BAD: User input used directly in an XPath expression\n\txPath := goxpath.MustParse(\"//users/user[login/text()='\" + username + \"']/home_dir/text()\")\n\tunsafeRes, _ := xPath.ExecBool(doc)\n\tfmt.Println(unsafeRes)\n\n\t// GOOD: Value of parameters is defined here instead of directly in the query\n\topt := func(o *goxpath.Opts) {\n\t\to.Vars[\"username\"] = tree.String(username)\n\t}\n\t// GOOD: Uses parameters to avoid including user input directly in XPath expression\n\txPath = goxpath.MustParse(\"//users/user[login/text()=$username]/home_dir/text()\")\n\tsafeRes, _ := xPath.ExecBool(doc, opt)\n\tfmt.Println(safeRes)\n}\n\n```\n\n## References\n* OWASP: [Testing for XPath Injection](https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/07-Input_Validation_Testing/09-Testing_for_XPath_Injection).\n* OWASP: [XPath Injection](https://www.owasp.org/index.php/XPATH_Injection).\n* Common Weakness Enumeration: [CWE-643](https://cwe.mitre.org/data/definitions/643.html).\n" + }, + "properties" : { + "tags" : [ "security", "external/cwe/cwe-643" ], + "description" : "Building an XPath expression from user-controlled sources is vulnerable to insertion of\n malicious code by the user.", + "id" : "go/xml/xpath-injection", + "kind" : "path-problem", + "name" : "XPath injection", + "precision" : "high", + "problem.severity" : "error", + "security-severity" : "9.8" + } + }, { + "id" : "go/unsafe-quoting", + "name" : "go/unsafe-quoting", + "shortDescription" : { + "text" : "Potentially unsafe quoting" + }, + "fullDescription" : { + "text" : "If a quoted string literal is constructed from data that may itself contain quotes, the embedded data could (accidentally or intentionally) change the structure of the overall string." + }, + "defaultConfiguration" : { + "enabled" : true, + "level" : "warning" + }, + "help" : { + "text" : "# Potentially unsafe quoting\nCode that constructs a string containing a quoted substring needs to ensure that any user-provided data embedded in between the quotes does not itself contain a quote. Otherwise the embedded data could (accidentally or intentionally) change the structure of the overall string by terminating the quoted substring early, with potentially severe consequences. If, for example, the string is later interpreted as an operating-system command or database query, a malicious attacker may be able to craft input data that enables a command injection or SQL injection attack.\n\n\n## Recommendation\nSanitize the embedded data appropriately to ensure quotes are escaped, or use an API that does not rely on manually constructing quoted substrings.\n\n\n## Example\nIn the following example, assume that `version` is an object from an untrusted source. The code snippet first uses `json.Marshal` to serialize this object into a string, and then embeds it into a SQL query built using the Squirrel library.\n\n\n```go\npackage main\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\tsq \"github.com/Masterminds/squirrel\"\n)\n\nfunc save(id string, version interface{}) {\n\tversionJSON, _ := json.Marshal(version)\n\tsq.StatementBuilder.\n\t\tInsert(\"resources\").\n\t\tColumns(\"resource_id\", \"version_md5\").\n\t\tValues(id, sq.Expr(fmt.Sprintf(\"md5('%s')\", versionJSON))).\n\t\tExec()\n}\n\n```\nNote that while Squirrel provides a structured API for building SQL queries that mitigates against common causes of SQL injection vulnerabilities, this code is still vulnerable: if the JSON-encoded representation of `version` contains a single quote, this will prematurely close the surrounding string, changing the structure of the SQL expression being constructed. This could be exploited to mount a SQL injection attack.\n\nTo fix this vulnerability, use Squirrel's placeholder syntax, which avoids the need to explicitly construct a quoted string.\n\n\n```go\npackage main\n\nimport (\n\t\"encoding/json\"\n\tsq \"github.com/Masterminds/squirrel\"\n)\n\nfunc saveGood(id string, version interface{}) {\n\tversionJSON, _ := json.Marshal(version)\n\tsq.StatementBuilder.\n\t\tInsert(\"resources\").\n\t\tColumns(\"resource_id\", \"version_md5\").\n\t\tValues(id, sq.Expr(\"md5(?)\", versionJSON)).\n\t\tExec()\n}\n\n```\n\n## References\n* Wikipedia: [SQL injection](https://en.wikipedia.org/wiki/SQL_injection).\n* OWASP: [Command Injection](https://www.owasp.org/index.php/Command_Injection).\n* Common Weakness Enumeration: [CWE-78](https://cwe.mitre.org/data/definitions/78.html).\n* Common Weakness Enumeration: [CWE-89](https://cwe.mitre.org/data/definitions/89.html).\n* Common Weakness Enumeration: [CWE-94](https://cwe.mitre.org/data/definitions/94.html).\n", + "markdown" : "# Potentially unsafe quoting\nCode that constructs a string containing a quoted substring needs to ensure that any user-provided data embedded in between the quotes does not itself contain a quote. Otherwise the embedded data could (accidentally or intentionally) change the structure of the overall string by terminating the quoted substring early, with potentially severe consequences. If, for example, the string is later interpreted as an operating-system command or database query, a malicious attacker may be able to craft input data that enables a command injection or SQL injection attack.\n\n\n## Recommendation\nSanitize the embedded data appropriately to ensure quotes are escaped, or use an API that does not rely on manually constructing quoted substrings.\n\n\n## Example\nIn the following example, assume that `version` is an object from an untrusted source. The code snippet first uses `json.Marshal` to serialize this object into a string, and then embeds it into a SQL query built using the Squirrel library.\n\n\n```go\npackage main\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\tsq \"github.com/Masterminds/squirrel\"\n)\n\nfunc save(id string, version interface{}) {\n\tversionJSON, _ := json.Marshal(version)\n\tsq.StatementBuilder.\n\t\tInsert(\"resources\").\n\t\tColumns(\"resource_id\", \"version_md5\").\n\t\tValues(id, sq.Expr(fmt.Sprintf(\"md5('%s')\", versionJSON))).\n\t\tExec()\n}\n\n```\nNote that while Squirrel provides a structured API for building SQL queries that mitigates against common causes of SQL injection vulnerabilities, this code is still vulnerable: if the JSON-encoded representation of `version` contains a single quote, this will prematurely close the surrounding string, changing the structure of the SQL expression being constructed. This could be exploited to mount a SQL injection attack.\n\nTo fix this vulnerability, use Squirrel's placeholder syntax, which avoids the need to explicitly construct a quoted string.\n\n\n```go\npackage main\n\nimport (\n\t\"encoding/json\"\n\tsq \"github.com/Masterminds/squirrel\"\n)\n\nfunc saveGood(id string, version interface{}) {\n\tversionJSON, _ := json.Marshal(version)\n\tsq.StatementBuilder.\n\t\tInsert(\"resources\").\n\t\tColumns(\"resource_id\", \"version_md5\").\n\t\tValues(id, sq.Expr(\"md5(?)\", versionJSON)).\n\t\tExec()\n}\n\n```\n\n## References\n* Wikipedia: [SQL injection](https://en.wikipedia.org/wiki/SQL_injection).\n* OWASP: [Command Injection](https://www.owasp.org/index.php/Command_Injection).\n* Common Weakness Enumeration: [CWE-78](https://cwe.mitre.org/data/definitions/78.html).\n* Common Weakness Enumeration: [CWE-89](https://cwe.mitre.org/data/definitions/89.html).\n* Common Weakness Enumeration: [CWE-94](https://cwe.mitre.org/data/definitions/94.html).\n" + }, + "properties" : { + "tags" : [ "correctness", "security", "external/cwe/cwe-078", "external/cwe/cwe-089", "external/cwe/cwe-094" ], + "description" : "If a quoted string literal is constructed from data that may itself contain quotes,\n the embedded data could (accidentally or intentionally) change the structure of\n the overall string.", + "id" : "go/unsafe-quoting", + "kind" : "path-problem", + "name" : "Potentially unsafe quoting", + "precision" : "high", + "problem.severity" : "warning", + "security-severity" : "9.3" + } + }, { + "id" : "go/sql-injection", + "name" : "go/sql-injection", + "shortDescription" : { + "text" : "Database query built from user-controlled sources" + }, + "fullDescription" : { + "text" : "Building a database query from user-controlled sources is vulnerable to insertion of malicious code by the user." + }, + "defaultConfiguration" : { + "enabled" : true, + "level" : "error" + }, + "help" : { + "text" : "# Database query built from user-controlled sources\nIf a database query (such as an SQL or NoSQL query) is built from user-provided data without sufficient sanitization, a malicious user may be able to run commands that exfiltrate, tamper with, or destroy data stored in the database.\n\n\n## Recommendation\nMost database connector libraries offer a way of safely embedding untrusted data into a query by means of query parameters or prepared statements. Use these features rather than building queries by string concatenation.\n\n\n## Example\nIn the following example, assume the function `handler` is an HTTP request handler in a web application, whose parameter `req` contains the request object:\n\n\n```go\npackage main\n\nimport (\n\t\"database/sql\"\n\t\"fmt\"\n\t\"net/http\"\n)\n\nfunc handler(db *sql.DB, req *http.Request) {\n\tq := fmt.Sprintf(\"SELECT ITEM,PRICE FROM PRODUCT WHERE ITEM_CATEGORY='%s' ORDER BY PRICE\",\n\t\treq.URL.Query()[\"category\"])\n\tdb.Query(q)\n}\n\n```\nThe handler constructs an SQL query involving user input taken from the request object unsafely using `fmt.Sprintf` to embed a request parameter directly into the query string `q`. The parameter may include quote characters, allowing a malicious user to terminate the string literal into which the parameter is embedded and add arbitrary SQL code after it.\n\nInstead, the untrusted query parameter should be safely embedded using placeholder parameters:\n\n\n```go\npackage main\n\nimport (\n\t\"database/sql\"\n\t\"net/http\"\n)\n\nfunc handlerGood(db *sql.DB, req *http.Request) {\n\tq := \"SELECT ITEM,PRICE FROM PRODUCT WHERE ITEM_CATEGORY='?' ORDER BY PRICE\"\n\tdb.Query(q, req.URL.Query()[\"category\"])\n}\n\n```\n\n## References\n* Wikipedia: [SQL injection](https://en.wikipedia.org/wiki/SQL_injection).\n* Common Weakness Enumeration: [CWE-89](https://cwe.mitre.org/data/definitions/89.html).\n", + "markdown" : "# Database query built from user-controlled sources\nIf a database query (such as an SQL or NoSQL query) is built from user-provided data without sufficient sanitization, a malicious user may be able to run commands that exfiltrate, tamper with, or destroy data stored in the database.\n\n\n## Recommendation\nMost database connector libraries offer a way of safely embedding untrusted data into a query by means of query parameters or prepared statements. Use these features rather than building queries by string concatenation.\n\n\n## Example\nIn the following example, assume the function `handler` is an HTTP request handler in a web application, whose parameter `req` contains the request object:\n\n\n```go\npackage main\n\nimport (\n\t\"database/sql\"\n\t\"fmt\"\n\t\"net/http\"\n)\n\nfunc handler(db *sql.DB, req *http.Request) {\n\tq := fmt.Sprintf(\"SELECT ITEM,PRICE FROM PRODUCT WHERE ITEM_CATEGORY='%s' ORDER BY PRICE\",\n\t\treq.URL.Query()[\"category\"])\n\tdb.Query(q)\n}\n\n```\nThe handler constructs an SQL query involving user input taken from the request object unsafely using `fmt.Sprintf` to embed a request parameter directly into the query string `q`. The parameter may include quote characters, allowing a malicious user to terminate the string literal into which the parameter is embedded and add arbitrary SQL code after it.\n\nInstead, the untrusted query parameter should be safely embedded using placeholder parameters:\n\n\n```go\npackage main\n\nimport (\n\t\"database/sql\"\n\t\"net/http\"\n)\n\nfunc handlerGood(db *sql.DB, req *http.Request) {\n\tq := \"SELECT ITEM,PRICE FROM PRODUCT WHERE ITEM_CATEGORY='?' ORDER BY PRICE\"\n\tdb.Query(q, req.URL.Query()[\"category\"])\n}\n\n```\n\n## References\n* Wikipedia: [SQL injection](https://en.wikipedia.org/wiki/SQL_injection).\n* Common Weakness Enumeration: [CWE-89](https://cwe.mitre.org/data/definitions/89.html).\n" + }, + "properties" : { + "tags" : [ "security", "external/cwe/cwe-089" ], + "description" : "Building a database query from user-controlled sources is vulnerable to insertion of\n malicious code by the user.", + "id" : "go/sql-injection", + "kind" : "path-problem", + "name" : "Database query built from user-controlled sources", + "precision" : "high", + "problem.severity" : "error", + "security-severity" : "8.8" + } + }, { + "id" : "go/email-injection", + "name" : "go/email-injection", + "shortDescription" : { + "text" : "Email content injection" + }, + "fullDescription" : { + "text" : "Incorporating untrusted input directly into an email message can enable content spoofing, which in turn may lead to information leaks and other security issues." + }, + "defaultConfiguration" : { + "enabled" : true, + "level" : "error" + }, + "help" : { + "text" : "# Email content injection\nUsing untrusted input to construct an email can cause multiple security vulnerabilities. For instance, inclusion of an untrusted input in an email body may allow an attacker to conduct cross-site scripting (XSS) attacks, while inclusion of an HTTP header may allow a full account compromise as shown in the example below.\n\n\n## Recommendation\nAny data which is passed to an email subject or body must be sanitized before use.\n\n\n## Example\nIn the following example snippet, the `host` field is user controlled.\n\nA malicious user can send an HTTP request to the targeted website, but with a Host header that refers to their own website. This means the emails will be sent out to potential victims, originating from a server they trust, but with links leading to a malicious website.\n\nIf the email contains a password reset link, and the victim clicks the link, the secret reset token will be leaked to the attacker. Using the leaked token, the attacker can then construct the real reset link and use it to change the victim's password.\n\n\n```go\npackage main\n\nimport (\n\t\"net/http\"\n\t\"net/smtp\"\n)\n\nfunc mail(w http.ResponseWriter, r *http.Request) {\n\thost := r.Header.Get(\"Host\")\n\ttoken := backend.getUserSecretResetToken(email)\n\tbody := \"Click to reset password: \" + host + \"/\" + token\n\tsmtp.SendMail(\"test.test\", nil, \"from@from.com\", nil, []byte(body))\n}\n\n```\nOne way to prevent this is to load the host name from a trusted configuration file instead.\n\n\n```go\npackage main\n\nimport (\n\t\"net/http\"\n\t\"net/smtp\"\n)\n\nfunc mailGood(w http.ResponseWriter, r *http.Request) {\n\thost := config.Get(\"Host\")\n\ttoken := backend.getUserSecretResetToken(email)\n\tbody := \"Click to reset password: \" + host + \"/\" + token\n\tsmtp.SendMail(\"test.test\", nil, \"from@from.com\", nil, []byte(body))\n}\n\n```\n\n## References\n* OWASP: [Content Spoofing](https://owasp.org/www-community/attacks/Content_Spoofing) .\n* Common Weakness Enumeration: [CWE-640](https://cwe.mitre.org/data/definitions/640.html).\n", + "markdown" : "# Email content injection\nUsing untrusted input to construct an email can cause multiple security vulnerabilities. For instance, inclusion of an untrusted input in an email body may allow an attacker to conduct cross-site scripting (XSS) attacks, while inclusion of an HTTP header may allow a full account compromise as shown in the example below.\n\n\n## Recommendation\nAny data which is passed to an email subject or body must be sanitized before use.\n\n\n## Example\nIn the following example snippet, the `host` field is user controlled.\n\nA malicious user can send an HTTP request to the targeted website, but with a Host header that refers to their own website. This means the emails will be sent out to potential victims, originating from a server they trust, but with links leading to a malicious website.\n\nIf the email contains a password reset link, and the victim clicks the link, the secret reset token will be leaked to the attacker. Using the leaked token, the attacker can then construct the real reset link and use it to change the victim's password.\n\n\n```go\npackage main\n\nimport (\n\t\"net/http\"\n\t\"net/smtp\"\n)\n\nfunc mail(w http.ResponseWriter, r *http.Request) {\n\thost := r.Header.Get(\"Host\")\n\ttoken := backend.getUserSecretResetToken(email)\n\tbody := \"Click to reset password: \" + host + \"/\" + token\n\tsmtp.SendMail(\"test.test\", nil, \"from@from.com\", nil, []byte(body))\n}\n\n```\nOne way to prevent this is to load the host name from a trusted configuration file instead.\n\n\n```go\npackage main\n\nimport (\n\t\"net/http\"\n\t\"net/smtp\"\n)\n\nfunc mailGood(w http.ResponseWriter, r *http.Request) {\n\thost := config.Get(\"Host\")\n\ttoken := backend.getUserSecretResetToken(email)\n\tbody := \"Click to reset password: \" + host + \"/\" + token\n\tsmtp.SendMail(\"test.test\", nil, \"from@from.com\", nil, []byte(body))\n}\n\n```\n\n## References\n* OWASP: [Content Spoofing](https://owasp.org/www-community/attacks/Content_Spoofing) .\n* Common Weakness Enumeration: [CWE-640](https://cwe.mitre.org/data/definitions/640.html).\n" + }, + "properties" : { + "tags" : [ "security", "external/cwe/cwe-640" ], + "description" : "Incorporating untrusted input directly into an email message can enable\n content spoofing, which in turn may lead to information leaks and other\n security issues.", + "id" : "go/email-injection", + "kind" : "path-problem", + "name" : "Email content injection", + "precision" : "high", + "problem.severity" : "error", + "security-severity" : "9.8" + } + }, { + "id" : "go/clear-text-logging", + "name" : "go/clear-text-logging", + "shortDescription" : { + "text" : "Clear-text logging of sensitive information" + }, + "fullDescription" : { + "text" : "Logging sensitive information without encryption or hashing can expose it to an attacker." + }, + "defaultConfiguration" : { + "enabled" : true, + "level" : "error" + }, + "help" : { + "text" : "# Clear-text logging of sensitive information\nSensitive information that is logged unencrypted is accessible to an attacker who gains access to the logs.\n\n\n## Recommendation\nEnsure that sensitive information is always encrypted or obfuscated before being logged.\n\nIn general, decrypt sensitive information only at the point where it is necessary for it to be used in cleartext.\n\nBe aware that external processes often store the standard out and standard error streams of the application, causing logged sensitive information to be stored.\n\n\n## Example\nThe following example code logs user credentials (in this case, their password) in plain text:\n\n\n```go\npackage main\n\nimport (\n\t\"log\"\n\t\"net/http\"\n)\n\nfunc serve() {\n\thttp.HandleFunc(\"/register\", func(w http.ResponseWriter, r *http.Request) {\n\t\tr.ParseForm()\n\t\tuser := r.Form.Get(\"user\")\n\t\tpw := r.Form.Get(\"password\")\n\n\t\tlog.Printf(\"Registering new user %s with password %s.\\n\", user, pw)\n\t})\n\thttp.ListenAndServe(\":80\", nil)\n}\n\n```\nInstead, the credentials should be encrypted, obfuscated, or omitted entirely:\n\n\n```go\npackage main\n\nimport (\n\t\"log\"\n\t\"net/http\"\n)\n\nfunc serve1() {\n\thttp.HandleFunc(\"/register\", func(w http.ResponseWriter, r *http.Request) {\n\t\tr.ParseForm()\n\t\tuser := r.Form.Get(\"user\")\n\t\tpw := r.Form.Get(\"password\")\n\n\t\tlog.Printf(\"Registering new user %s.\\n\", user)\n\n\t\t// ...\n\t\tuse(pw)\n\t})\n\thttp.ListenAndServe(\":80\", nil)\n}\n\n```\n\n## References\n* M. Dowd, J. McDonald and J. Schuhm, *The Art of Software Security Assessment*, 1st Edition, Chapter 2 - 'Common Vulnerabilities of Encryption', p. 43. Addison Wesley, 2006.\n* M. Howard and D. LeBlanc, *Writing Secure Code*, 2nd Edition, Chapter 9 - 'Protecting Secret Data', p. 299. Microsoft, 2002.\n* OWASP: [Password Plaintext Storage](https://www.owasp.org/index.php/Password_Plaintext_Storage).\n* Common Weakness Enumeration: [CWE-312](https://cwe.mitre.org/data/definitions/312.html).\n* Common Weakness Enumeration: [CWE-315](https://cwe.mitre.org/data/definitions/315.html).\n* Common Weakness Enumeration: [CWE-359](https://cwe.mitre.org/data/definitions/359.html).\n", + "markdown" : "# Clear-text logging of sensitive information\nSensitive information that is logged unencrypted is accessible to an attacker who gains access to the logs.\n\n\n## Recommendation\nEnsure that sensitive information is always encrypted or obfuscated before being logged.\n\nIn general, decrypt sensitive information only at the point where it is necessary for it to be used in cleartext.\n\nBe aware that external processes often store the standard out and standard error streams of the application, causing logged sensitive information to be stored.\n\n\n## Example\nThe following example code logs user credentials (in this case, their password) in plain text:\n\n\n```go\npackage main\n\nimport (\n\t\"log\"\n\t\"net/http\"\n)\n\nfunc serve() {\n\thttp.HandleFunc(\"/register\", func(w http.ResponseWriter, r *http.Request) {\n\t\tr.ParseForm()\n\t\tuser := r.Form.Get(\"user\")\n\t\tpw := r.Form.Get(\"password\")\n\n\t\tlog.Printf(\"Registering new user %s with password %s.\\n\", user, pw)\n\t})\n\thttp.ListenAndServe(\":80\", nil)\n}\n\n```\nInstead, the credentials should be encrypted, obfuscated, or omitted entirely:\n\n\n```go\npackage main\n\nimport (\n\t\"log\"\n\t\"net/http\"\n)\n\nfunc serve1() {\n\thttp.HandleFunc(\"/register\", func(w http.ResponseWriter, r *http.Request) {\n\t\tr.ParseForm()\n\t\tuser := r.Form.Get(\"user\")\n\t\tpw := r.Form.Get(\"password\")\n\n\t\tlog.Printf(\"Registering new user %s.\\n\", user)\n\n\t\t// ...\n\t\tuse(pw)\n\t})\n\thttp.ListenAndServe(\":80\", nil)\n}\n\n```\n\n## References\n* M. Dowd, J. McDonald and J. Schuhm, *The Art of Software Security Assessment*, 1st Edition, Chapter 2 - 'Common Vulnerabilities of Encryption', p. 43. Addison Wesley, 2006.\n* M. Howard and D. LeBlanc, *Writing Secure Code*, 2nd Edition, Chapter 9 - 'Protecting Secret Data', p. 299. Microsoft, 2002.\n* OWASP: [Password Plaintext Storage](https://www.owasp.org/index.php/Password_Plaintext_Storage).\n* Common Weakness Enumeration: [CWE-312](https://cwe.mitre.org/data/definitions/312.html).\n* Common Weakness Enumeration: [CWE-315](https://cwe.mitre.org/data/definitions/315.html).\n* Common Weakness Enumeration: [CWE-359](https://cwe.mitre.org/data/definitions/359.html).\n" + }, + "properties" : { + "tags" : [ "security", "external/cwe/cwe-312", "external/cwe/cwe-315", "external/cwe/cwe-359" ], + "description" : "Logging sensitive information without encryption or hashing can\n expose it to an attacker.", + "id" : "go/clear-text-logging", + "kind" : "path-problem", + "name" : "Clear-text logging of sensitive information", + "precision" : "high", + "problem.severity" : "error", + "security-severity" : "7.5" + } + }, { + "id" : "go/regex/missing-regexp-anchor", + "name" : "go/regex/missing-regexp-anchor", + "shortDescription" : { + "text" : "Missing regular expression anchor" + }, + "fullDescription" : { + "text" : "Regular expressions without anchors can be vulnerable to bypassing." + }, + "defaultConfiguration" : { + "enabled" : true, + "level" : "warning" + }, + "help" : { + "text" : "# Missing regular expression anchor\nSanitizing untrusted input with regular expressions is a common technique. However, it is error-prone to match untrusted input against regular expressions without anchors such as `^` or `$`. Malicious input can bypass such security checks by embedding one of the allowed patterns in an unexpected location.\n\nEven if the matching is not done in a security-critical context, it may still cause undesirable behavior when the regular expression accidentally matches.\n\n\n## Recommendation\nUse anchors to ensure that regular expressions match at the expected locations.\n\n\n## Example\nThe following example code checks that a URL redirection will reach the `example.com` domain, or one of its subdomains, and not some malicious site.\n\n\n```go\npackage main\n\nimport (\n\t\"errors\"\n\t\"net/http\"\n\t\"regexp\"\n)\n\nfunc checkRedirect2(req *http.Request, via []*http.Request) error {\n\t// BAD: the host of `req.URL` may be controlled by an attacker\n\tre := \"https?://www\\\\.example\\\\.com/\"\n\tif matched, _ := regexp.MatchString(re, req.URL.String()); matched {\n\t\treturn nil\n\t}\n\treturn errors.New(\"Invalid redirect\")\n}\n\n```\nThe check with the regular expression match is, however, easy to bypass. For example, the string `http://example.com/` can be embedded in the query string component: `http://evil-example.net/?x=http://example.com/`.\n\nAddress these shortcomings by using anchors in the regular expression instead:\n\n\n```go\npackage main\n\nimport (\n\t\"errors\"\n\t\"net/http\"\n\t\"regexp\"\n)\n\nfunc checkRedirect2Good(req *http.Request, via []*http.Request) error {\n\t// GOOD: the host of `req.URL` cannot be controlled by an attacker\n\tre := \"^https?://www\\\\.example\\\\.com/\"\n\tif matched, _ := regexp.MatchString(re, req.URL.String()); matched {\n\t\treturn nil\n\t}\n\treturn errors.New(\"Invalid redirect\")\n}\n\n```\nA related mistake is to write a regular expression with multiple alternatives, but to only anchor one of the alternatives. As an example, the regular expression `^www\\.example\\.com|beta\\.example\\.com` will match the host `evil.beta.example.com` because the regular expression is parsed as `(^www\\.example\\.com)|(beta\\.example\\.com)/`, so the second alternative `beta\\.example\\.com` is not anchored at the beginning of the string.\n\n\n## References\n* OWASP: [SSRF](https://www.owasp.org/index.php/Server_Side_Request_Forgery)\n* OWASP: [Unvalidated Redirects and Forwards Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html).\n* Common Weakness Enumeration: [CWE-20](https://cwe.mitre.org/data/definitions/20.html).\n", + "markdown" : "# Missing regular expression anchor\nSanitizing untrusted input with regular expressions is a common technique. However, it is error-prone to match untrusted input against regular expressions without anchors such as `^` or `$`. Malicious input can bypass such security checks by embedding one of the allowed patterns in an unexpected location.\n\nEven if the matching is not done in a security-critical context, it may still cause undesirable behavior when the regular expression accidentally matches.\n\n\n## Recommendation\nUse anchors to ensure that regular expressions match at the expected locations.\n\n\n## Example\nThe following example code checks that a URL redirection will reach the `example.com` domain, or one of its subdomains, and not some malicious site.\n\n\n```go\npackage main\n\nimport (\n\t\"errors\"\n\t\"net/http\"\n\t\"regexp\"\n)\n\nfunc checkRedirect2(req *http.Request, via []*http.Request) error {\n\t// BAD: the host of `req.URL` may be controlled by an attacker\n\tre := \"https?://www\\\\.example\\\\.com/\"\n\tif matched, _ := regexp.MatchString(re, req.URL.String()); matched {\n\t\treturn nil\n\t}\n\treturn errors.New(\"Invalid redirect\")\n}\n\n```\nThe check with the regular expression match is, however, easy to bypass. For example, the string `http://example.com/` can be embedded in the query string component: `http://evil-example.net/?x=http://example.com/`.\n\nAddress these shortcomings by using anchors in the regular expression instead:\n\n\n```go\npackage main\n\nimport (\n\t\"errors\"\n\t\"net/http\"\n\t\"regexp\"\n)\n\nfunc checkRedirect2Good(req *http.Request, via []*http.Request) error {\n\t// GOOD: the host of `req.URL` cannot be controlled by an attacker\n\tre := \"^https?://www\\\\.example\\\\.com/\"\n\tif matched, _ := regexp.MatchString(re, req.URL.String()); matched {\n\t\treturn nil\n\t}\n\treturn errors.New(\"Invalid redirect\")\n}\n\n```\nA related mistake is to write a regular expression with multiple alternatives, but to only anchor one of the alternatives. As an example, the regular expression `^www\\.example\\.com|beta\\.example\\.com` will match the host `evil.beta.example.com` because the regular expression is parsed as `(^www\\.example\\.com)|(beta\\.example\\.com)/`, so the second alternative `beta\\.example\\.com` is not anchored at the beginning of the string.\n\n\n## References\n* OWASP: [SSRF](https://www.owasp.org/index.php/Server_Side_Request_Forgery)\n* OWASP: [Unvalidated Redirects and Forwards Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html).\n* Common Weakness Enumeration: [CWE-20](https://cwe.mitre.org/data/definitions/20.html).\n" + }, + "properties" : { + "tags" : [ "correctness", "security", "external/cwe/cwe-20" ], + "description" : "Regular expressions without anchors can be vulnerable to bypassing.", + "id" : "go/regex/missing-regexp-anchor", + "kind" : "problem", + "name" : "Missing regular expression anchor", + "precision" : "high", + "problem.severity" : "warning", + "security-severity" : "7.8" + } + }, { + "id" : "go/incomplete-url-scheme-check", + "name" : "go/incomplete-url-scheme-check", + "shortDescription" : { + "text" : "Incomplete URL scheme check" + }, + "fullDescription" : { + "text" : "Checking for the \"javascript:\" URL scheme without also checking for \"vbscript:\" and \"data:\" suggests a logic error or even a security vulnerability." + }, + "defaultConfiguration" : { + "enabled" : true, + "level" : "warning" + }, + "help" : { + "text" : "# Incomplete URL scheme check\nURLs with the special scheme `javascript` can be used to encode JavaScript code to be executed when the URL is visited. While this is a powerful mechanism for creating feature-rich and responsive web applications, it is also a potential security risk: if the URL comes from an untrusted source, it might contain harmful JavaScript code. For this reason, many frameworks and libraries first check the URL scheme of any untrusted URL, and reject URLs with the `javascript` scheme.\n\nHowever, the `data` and `vbscript` schemes can be used to represent executable code in a very similar way, so any validation logic that checks against `javascript`, but not against `data` and `vbscript`, is likely to be insufficient.\n\n\n## Recommendation\nAdd checks covering both `data:` and `vbscript:`.\n\n\n## Example\nThe following function validates a (presumably untrusted) URL `urlstr`. If its scheme is `javascript`, the harmless placeholder URL `about:blank` is returned to prevent code injection; otherwise `urlstr` itself is returned.\n\n\n```go\npackage main\n\nimport \"net/url\"\n\nfunc sanitizeUrl(urlstr string) string {\n\tu, err := url.Parse(urlstr)\n\tif err != nil || u.Scheme == \"javascript\" {\n\t\treturn \"about:blank\"\n\t}\n\treturn urlstr\n}\n\n```\nWhile this check provides partial projection, it should be extended to cover `data` and `vbscript` as well:\n\n\n```go\npackage main\n\nimport \"net/url\"\n\nfunc sanitizeUrlGod(urlstr string) string {\n\tu, err := url.Parse(urlstr)\n\tif err != nil || u.Scheme == \"javascript\" || u.Scheme == \"data\" || u.Scheme == \"vbscript\" {\n\t\treturn \"about:blank\"\n\t}\n\treturn urlstr\n}\n\n```\n\n## References\n* WHATWG: [URL schemes](https://wiki.whatwg.org/wiki/URL_schemes).\n* Common Weakness Enumeration: [CWE-20](https://cwe.mitre.org/data/definitions/20.html).\n", + "markdown" : "# Incomplete URL scheme check\nURLs with the special scheme `javascript` can be used to encode JavaScript code to be executed when the URL is visited. While this is a powerful mechanism for creating feature-rich and responsive web applications, it is also a potential security risk: if the URL comes from an untrusted source, it might contain harmful JavaScript code. For this reason, many frameworks and libraries first check the URL scheme of any untrusted URL, and reject URLs with the `javascript` scheme.\n\nHowever, the `data` and `vbscript` schemes can be used to represent executable code in a very similar way, so any validation logic that checks against `javascript`, but not against `data` and `vbscript`, is likely to be insufficient.\n\n\n## Recommendation\nAdd checks covering both `data:` and `vbscript:`.\n\n\n## Example\nThe following function validates a (presumably untrusted) URL `urlstr`. If its scheme is `javascript`, the harmless placeholder URL `about:blank` is returned to prevent code injection; otherwise `urlstr` itself is returned.\n\n\n```go\npackage main\n\nimport \"net/url\"\n\nfunc sanitizeUrl(urlstr string) string {\n\tu, err := url.Parse(urlstr)\n\tif err != nil || u.Scheme == \"javascript\" {\n\t\treturn \"about:blank\"\n\t}\n\treturn urlstr\n}\n\n```\nWhile this check provides partial projection, it should be extended to cover `data` and `vbscript` as well:\n\n\n```go\npackage main\n\nimport \"net/url\"\n\nfunc sanitizeUrlGod(urlstr string) string {\n\tu, err := url.Parse(urlstr)\n\tif err != nil || u.Scheme == \"javascript\" || u.Scheme == \"data\" || u.Scheme == \"vbscript\" {\n\t\treturn \"about:blank\"\n\t}\n\treturn urlstr\n}\n\n```\n\n## References\n* WHATWG: [URL schemes](https://wiki.whatwg.org/wiki/URL_schemes).\n* Common Weakness Enumeration: [CWE-20](https://cwe.mitre.org/data/definitions/20.html).\n" + }, + "properties" : { + "tags" : [ "security", "correctness", "external/cwe/cwe-020" ], + "description" : "Checking for the \"javascript:\" URL scheme without also checking for \"vbscript:\"\n and \"data:\" suggests a logic error or even a security vulnerability.", + "id" : "go/incomplete-url-scheme-check", + "kind" : "problem", + "name" : "Incomplete URL scheme check", + "precision" : "high", + "problem.severity" : "warning", + "security-severity" : "7.8" + } + }, { + "id" : "go/incomplete-hostname-regexp", + "name" : "go/incomplete-hostname-regexp", + "shortDescription" : { + "text" : "Incomplete regular expression for hostnames" + }, + "fullDescription" : { + "text" : "Matching a URL or hostname against a regular expression that contains an unescaped dot as part of the hostname might match more hostnames than expected." + }, + "defaultConfiguration" : { + "enabled" : true, + "level" : "warning" + }, + "help" : { + "text" : "# Incomplete regular expression for hostnames\nSanitizing untrusted URLs is an important technique for preventing attacks such as request forgeries and malicious redirections. Often, this is done by checking that the host of a URL is in a set of allowed hosts.\n\nIf a regular expression implements such a check, it is easy to accidentally make the check too permissive by not escaping regular-expression meta-characters such as `.`.\n\nEven if the check is not used in a security-critical context, the incomplete check may still cause undesirable behavior when it accidentally succeeds.\n\n\n## Recommendation\nEscape all meta-characters appropriately when constructing regular expressions for security checks, paying special attention to the `.` meta-character.\n\n\n## Example\nThe following example code checks that a URL redirection will reach the `example.com` domain, or one of its subdomains.\n\n\n```go\npackage main\n\nimport (\n\t\"errors\"\n\t\"net/http\"\n\t\"regexp\"\n)\n\nfunc checkRedirect(req *http.Request, via []*http.Request) error {\n\t// BAD: the host of `req.URL` may be controlled by an attacker\n\tre := \"^((www|beta).)?example.com/\"\n\tif matched, _ := regexp.MatchString(re, req.URL.Host); matched {\n\t\treturn nil\n\t}\n\treturn errors.New(\"Invalid redirect\")\n}\n\n```\nThe check is however easy to bypass because the unescaped `.` allows for any character before `example.com`, effectively allowing the redirect to go to an attacker-controlled domain such as `wwwXexample.com`.\n\nAddress this vulnerability by escaping `.` appropriately:\n\n\n```go\npackage main\n\nimport (\n\t\"errors\"\n\t\"net/http\"\n\t\"regexp\"\n)\n\nfunc checkRedirectGood(req *http.Request, via []*http.Request) error {\n\t// GOOD: the host of `req.URL` must be `example.com`, `www.example.com` or `beta.example.com`\n\tre := \"^((www|beta)\\\\.)?example\\\\.com/\"\n\tif matched, _ := regexp.MatchString(re, req.URL.Host); matched {\n\t\treturn nil\n\t}\n\treturn errors.New(\"Invalid redirect\")\n}\n\n```\n\n## References\n* OWASP: [SSRF](https://www.owasp.org/index.php/Server_Side_Request_Forgery)\n* OWASP: [Unvalidated Redirects and Forwards Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html).\n* Common Weakness Enumeration: [CWE-20](https://cwe.mitre.org/data/definitions/20.html).\n", + "markdown" : "# Incomplete regular expression for hostnames\nSanitizing untrusted URLs is an important technique for preventing attacks such as request forgeries and malicious redirections. Often, this is done by checking that the host of a URL is in a set of allowed hosts.\n\nIf a regular expression implements such a check, it is easy to accidentally make the check too permissive by not escaping regular-expression meta-characters such as `.`.\n\nEven if the check is not used in a security-critical context, the incomplete check may still cause undesirable behavior when it accidentally succeeds.\n\n\n## Recommendation\nEscape all meta-characters appropriately when constructing regular expressions for security checks, paying special attention to the `.` meta-character.\n\n\n## Example\nThe following example code checks that a URL redirection will reach the `example.com` domain, or one of its subdomains.\n\n\n```go\npackage main\n\nimport (\n\t\"errors\"\n\t\"net/http\"\n\t\"regexp\"\n)\n\nfunc checkRedirect(req *http.Request, via []*http.Request) error {\n\t// BAD: the host of `req.URL` may be controlled by an attacker\n\tre := \"^((www|beta).)?example.com/\"\n\tif matched, _ := regexp.MatchString(re, req.URL.Host); matched {\n\t\treturn nil\n\t}\n\treturn errors.New(\"Invalid redirect\")\n}\n\n```\nThe check is however easy to bypass because the unescaped `.` allows for any character before `example.com`, effectively allowing the redirect to go to an attacker-controlled domain such as `wwwXexample.com`.\n\nAddress this vulnerability by escaping `.` appropriately:\n\n\n```go\npackage main\n\nimport (\n\t\"errors\"\n\t\"net/http\"\n\t\"regexp\"\n)\n\nfunc checkRedirectGood(req *http.Request, via []*http.Request) error {\n\t// GOOD: the host of `req.URL` must be `example.com`, `www.example.com` or `beta.example.com`\n\tre := \"^((www|beta)\\\\.)?example\\\\.com/\"\n\tif matched, _ := regexp.MatchString(re, req.URL.Host); matched {\n\t\treturn nil\n\t}\n\treturn errors.New(\"Invalid redirect\")\n}\n\n```\n\n## References\n* OWASP: [SSRF](https://www.owasp.org/index.php/Server_Side_Request_Forgery)\n* OWASP: [Unvalidated Redirects and Forwards Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html).\n* Common Weakness Enumeration: [CWE-20](https://cwe.mitre.org/data/definitions/20.html).\n" + }, + "properties" : { + "tags" : [ "correctness", "security", "external/cwe/cwe-20" ], + "description" : "Matching a URL or hostname against a regular expression that contains an unescaped\n dot as part of the hostname might match more hostnames than expected.", + "id" : "go/incomplete-hostname-regexp", + "kind" : "path-problem", + "name" : "Incomplete regular expression for hostnames", + "precision" : "high", + "problem.severity" : "warning", + "security-severity" : "7.8" + } + }, { + "id" : "go/suspicious-character-in-regex", + "name" : "go/suspicious-character-in-regex", + "shortDescription" : { + "text" : "Suspicious characters in a regular expression" + }, + "fullDescription" : { + "text" : "If a literal bell character or backspace appears in a regular expression, the start of text or word boundary may have been intended." + }, + "defaultConfiguration" : { + "enabled" : true, + "level" : "warning" + }, + "help" : { + "text" : "# Suspicious characters in a regular expression\nWhen a character in a string literal or regular expression literal is preceded by a backslash, it is interpreted as part of an escape sequence. For example, the escape sequence `\\n` in a string literal corresponds to a single `newline` character, and not the `\\` and `n` characters. There are two Go escape sequences that could produce surprising results. First, `regexp.Compile(\"\\a\")` matches the bell character, whereas `regexp.Compile(\"\\\\A\")` matches the start of text and `regexp.Compile(\"\\\\a\")` is a Vim (but not Go) regular expression matching any alphabetic character. Second, `regexp.Compile(\"\\b\")` matches a backspace, whereas `regexp.Compile(\"\\\\b\")` matches the start of a word. Confusing one for the other could lead to a regular expression passing or failing much more often than expected, with potential security consequences. Note this is less of a problem than in some other languages because in Go, only valid escape sequences are accepted, both in an ordinary string (for example, `s := \"\\k\"` will not compile as there is no such escape sequence) and in regular expressions (for example, `regexp.MustCompile(\"\\\\k\")` will panic as `\\k` does not refer to a character class or other special token according to Go's regular expression grammar).\n\n\n## Recommendation\nEnsure that the right number of backslashes is used when escaping characters in strings and regular expressions.\n\n\n## Example\nThe following example code fails to check for a forbidden word in an input string:\n\n\n```go\npackage main\n\nimport \"regexp\"\n\nfunc broken(hostNames []byte) string {\n\tvar hostRe = regexp.MustCompile(\"\\bforbidden.host.org\")\n\tif hostRe.Match(hostNames) {\n\t\treturn \"Must not target forbidden.host.org\"\n\t} else {\n\t\t// This will be reached even if hostNames is exactly \"forbidden.host.org\",\n\t\t// because the literal backspace is not matched\n\t\treturn \"\"\n\t}\n}\n\n```\nThe check does not work, but can be fixed by escaping the backslash:\n\n\n```go\npackage main\n\nimport \"regexp\"\n\nfunc fixed(hostNames []byte) string {\n\tvar hostRe = regexp.MustCompile(`\\bforbidden.host.org`)\n\tif hostRe.Match(hostNames) {\n\t\treturn \"Must not target forbidden.host.org\"\n\t} else {\n\t\t// hostNames definitely doesn't contain a word \"forbidden.host.org\", as \"\\\\b\"\n\t\t// is the start-of-word anchor, not a literal backspace.\n\t\treturn \"\"\n\t}\n}\n\n```\nAlternatively, you can use backtick-delimited raw string literals. For example, the `\\b` in ``` regexp.Compile(`hello\\bworld`) ``` matches a word boundary, not a backspace character, as within backticks `\\b` is not an escape sequence.\n\n\n## References\n* golang.org: [Overview of the Regexp package](https://golang.org/pkg/regexp/).\n* Google: [Syntax of regular expressions accepted by RE2](https://github.com/google/re2/wiki/Syntax).\n* Common Weakness Enumeration: [CWE-20](https://cwe.mitre.org/data/definitions/20.html).\n", + "markdown" : "# Suspicious characters in a regular expression\nWhen a character in a string literal or regular expression literal is preceded by a backslash, it is interpreted as part of an escape sequence. For example, the escape sequence `\\n` in a string literal corresponds to a single `newline` character, and not the `\\` and `n` characters. There are two Go escape sequences that could produce surprising results. First, `regexp.Compile(\"\\a\")` matches the bell character, whereas `regexp.Compile(\"\\\\A\")` matches the start of text and `regexp.Compile(\"\\\\a\")` is a Vim (but not Go) regular expression matching any alphabetic character. Second, `regexp.Compile(\"\\b\")` matches a backspace, whereas `regexp.Compile(\"\\\\b\")` matches the start of a word. Confusing one for the other could lead to a regular expression passing or failing much more often than expected, with potential security consequences. Note this is less of a problem than in some other languages because in Go, only valid escape sequences are accepted, both in an ordinary string (for example, `s := \"\\k\"` will not compile as there is no such escape sequence) and in regular expressions (for example, `regexp.MustCompile(\"\\\\k\")` will panic as `\\k` does not refer to a character class or other special token according to Go's regular expression grammar).\n\n\n## Recommendation\nEnsure that the right number of backslashes is used when escaping characters in strings and regular expressions.\n\n\n## Example\nThe following example code fails to check for a forbidden word in an input string:\n\n\n```go\npackage main\n\nimport \"regexp\"\n\nfunc broken(hostNames []byte) string {\n\tvar hostRe = regexp.MustCompile(\"\\bforbidden.host.org\")\n\tif hostRe.Match(hostNames) {\n\t\treturn \"Must not target forbidden.host.org\"\n\t} else {\n\t\t// This will be reached even if hostNames is exactly \"forbidden.host.org\",\n\t\t// because the literal backspace is not matched\n\t\treturn \"\"\n\t}\n}\n\n```\nThe check does not work, but can be fixed by escaping the backslash:\n\n\n```go\npackage main\n\nimport \"regexp\"\n\nfunc fixed(hostNames []byte) string {\n\tvar hostRe = regexp.MustCompile(`\\bforbidden.host.org`)\n\tif hostRe.Match(hostNames) {\n\t\treturn \"Must not target forbidden.host.org\"\n\t} else {\n\t\t// hostNames definitely doesn't contain a word \"forbidden.host.org\", as \"\\\\b\"\n\t\t// is the start-of-word anchor, not a literal backspace.\n\t\treturn \"\"\n\t}\n}\n\n```\nAlternatively, you can use backtick-delimited raw string literals. For example, the `\\b` in ``` regexp.Compile(`hello\\bworld`) ``` matches a word boundary, not a backspace character, as within backticks `\\b` is not an escape sequence.\n\n\n## References\n* golang.org: [Overview of the Regexp package](https://golang.org/pkg/regexp/).\n* Google: [Syntax of regular expressions accepted by RE2](https://github.com/google/re2/wiki/Syntax).\n* Common Weakness Enumeration: [CWE-20](https://cwe.mitre.org/data/definitions/20.html).\n" + }, + "properties" : { + "tags" : [ "correctness", "security", "external/cwe/cwe-20" ], + "description" : "If a literal bell character or backspace appears in a regular expression, the start of text or word boundary may have been intended.", + "id" : "go/suspicious-character-in-regex", + "kind" : "path-problem", + "name" : "Suspicious characters in a regular expression", + "precision" : "high", + "problem.severity" : "warning", + "security-severity" : "7.8" + } + }, { + "id" : "go/request-forgery", + "name" : "go/request-forgery", + "shortDescription" : { + "text" : "Uncontrolled data used in network request" + }, + "fullDescription" : { + "text" : "Sending network requests with user-controlled data allows for request forgery attacks." + }, + "defaultConfiguration" : { + "enabled" : true, + "level" : "error" + }, + "help" : { + "text" : "# Uncontrolled data used in network request\nDirectly incorporating user input into an HTTP request without validating the input can facilitate different kinds of request forgery attacks, where the attacker essentially controls the request. If the vulnerable request is in server-side code, then security mechanisms, such as external firewalls, can be bypassed. If the vulnerable request is in client-side code, then unsuspecting users can send malicious requests to other servers, potentially resulting in a DDOS attack.\n\n\n## Recommendation\nTo guard against request forgery, it is advisable to avoid putting user input directly into a network request. If a flexible network request mechanism is required, it is recommended to maintain a list of authorized request targets and choose from that list based on the user input provided.\n\n\n## Example\nThe following example shows an HTTP request parameter being used directly in a URL request without validating the input, which facilitates an SSRF attack. The request `http.Get(...)` is vulnerable since attackers can choose the value of `target` to be anything they want. For instance, the attacker can choose `\"internal.example.com/#\"` as the target, causing the URL used in the request to be `\"https://internal.example.com/#.example.com/data\"`.\n\nA request to `https://internal.example.com` may be problematic if that server is not meant to be directly accessible from the attacker's machine.\n\n\n```go\npackage main\n\nimport (\n\t\"net/http\"\n)\n\nfunc handler(w http.ResponseWriter, req *http.Request) {\n\ttarget := req.FormValue(\"target\")\n\n\t// BAD: `target` is controlled by the attacker\n\tresp, err := http.Get(\"https://\" + target + \".example.com/data/\")\n\tif err != nil {\n\t\t// error handling\n\t}\n\n\t// process request response\n\tuse(resp)\n}\n\n```\nOne way to remedy the problem is to use the user input to select a known fixed string before performing the request:\n\n\n```go\npackage main\n\nimport (\n\t\"net/http\"\n)\n\nfunc handler1(w http.ResponseWriter, req *http.Request) {\n\ttarget := req.FormValue(\"target\")\n\n\tvar subdomain string\n\tif target == \"EU\" {\n\t\tsubdomain = \"europe\"\n\t} else {\n\t\tsubdomain = \"world\"\n\t}\n\n\t// GOOD: `subdomain` is controlled by the server\n\tresp, err := http.Get(\"https://\" + subdomain + \".example.com/data/\")\n\tif err != nil {\n\t\t// error handling\n\t}\n\n\t// process request response\n\tuse(resp)\n}\n\n```\n\n## References\n* OWASP: [SSRF](https://www.owasp.org/index.php/Server_Side_Request_Forgery)\n* Common Weakness Enumeration: [CWE-918](https://cwe.mitre.org/data/definitions/918.html).\n", + "markdown" : "# Uncontrolled data used in network request\nDirectly incorporating user input into an HTTP request without validating the input can facilitate different kinds of request forgery attacks, where the attacker essentially controls the request. If the vulnerable request is in server-side code, then security mechanisms, such as external firewalls, can be bypassed. If the vulnerable request is in client-side code, then unsuspecting users can send malicious requests to other servers, potentially resulting in a DDOS attack.\n\n\n## Recommendation\nTo guard against request forgery, it is advisable to avoid putting user input directly into a network request. If a flexible network request mechanism is required, it is recommended to maintain a list of authorized request targets and choose from that list based on the user input provided.\n\n\n## Example\nThe following example shows an HTTP request parameter being used directly in a URL request without validating the input, which facilitates an SSRF attack. The request `http.Get(...)` is vulnerable since attackers can choose the value of `target` to be anything they want. For instance, the attacker can choose `\"internal.example.com/#\"` as the target, causing the URL used in the request to be `\"https://internal.example.com/#.example.com/data\"`.\n\nA request to `https://internal.example.com` may be problematic if that server is not meant to be directly accessible from the attacker's machine.\n\n\n```go\npackage main\n\nimport (\n\t\"net/http\"\n)\n\nfunc handler(w http.ResponseWriter, req *http.Request) {\n\ttarget := req.FormValue(\"target\")\n\n\t// BAD: `target` is controlled by the attacker\n\tresp, err := http.Get(\"https://\" + target + \".example.com/data/\")\n\tif err != nil {\n\t\t// error handling\n\t}\n\n\t// process request response\n\tuse(resp)\n}\n\n```\nOne way to remedy the problem is to use the user input to select a known fixed string before performing the request:\n\n\n```go\npackage main\n\nimport (\n\t\"net/http\"\n)\n\nfunc handler1(w http.ResponseWriter, req *http.Request) {\n\ttarget := req.FormValue(\"target\")\n\n\tvar subdomain string\n\tif target == \"EU\" {\n\t\tsubdomain = \"europe\"\n\t} else {\n\t\tsubdomain = \"world\"\n\t}\n\n\t// GOOD: `subdomain` is controlled by the server\n\tresp, err := http.Get(\"https://\" + subdomain + \".example.com/data/\")\n\tif err != nil {\n\t\t// error handling\n\t}\n\n\t// process request response\n\tuse(resp)\n}\n\n```\n\n## References\n* OWASP: [SSRF](https://www.owasp.org/index.php/Server_Side_Request_Forgery)\n* Common Weakness Enumeration: [CWE-918](https://cwe.mitre.org/data/definitions/918.html).\n" + }, + "properties" : { + "tags" : [ "security", "external/cwe/cwe-918" ], + "description" : "Sending network requests with user-controlled data allows for request forgery attacks.", + "id" : "go/request-forgery", + "kind" : "path-problem", + "name" : "Uncontrolled data used in network request", + "precision" : "high", + "problem.severity" : "error", + "security-severity" : "9.1" + } + }, { + "id" : "go/stack-trace-exposure", + "name" : "go/stack-trace-exposure", + "shortDescription" : { + "text" : "Information exposure through a stack trace" + }, + "fullDescription" : { + "text" : "Information from a stack trace propagates to an external user. Stack traces can unintentionally reveal implementation details that are useful to an attacker for developing a subsequent exploit." + }, + "defaultConfiguration" : { + "enabled" : true, + "level" : "error" + }, + "help" : { + "text" : "# Information exposure through a stack trace\nSoftware developers often add stack traces to error messages, as a debugging aid. Whenever that error message occurs for an end user, the developer can use the stack trace to help identify how to fix the problem. In particular, stack traces can tell the developer more about the sequence of events that led to a failure, as opposed to merely the final state of the software when the error occurred.\n\nUnfortunately, the same information can be useful to an attacker. The sequence of class names in a stack trace can reveal the structure of the application as well as any internal components it relies on.\n\n\n## Recommendation\nSend the user a more generic error message that reveals less information. Either suppress the stack trace entirely, or log it only on the server.\n\n\n## Example\nIn the following example, a panic is handled in two different ways. In the first version, labeled BAD, a detailed stack trace is written to a user-facing HTTP response object, which may disclose sensitive information. In the second version, the error message is logged only on the server. That way, the developers can still access and use the error log, but remote users will not see the information.\n\n\n```go\npackage example\n\nimport (\n\t\"log\"\n\t\"net/http\"\n\t\"runtime\"\n)\n\nfunc handlePanic(w http.ResponseWriter, r *http.Request) {\n\tbuf := make([]byte, 2<<16)\n\tbuf = buf[:runtime.Stack(buf, true)]\n\t// BAD: printing a stack trace back to the response\n\tw.Write(buf)\n\t// GOOD: logging the response to the server and sending\n\t// a more generic message.\n\tlog.Printf(\"Panic: %s\", buf)\n\tw.Write([]byte(\"An unexpected runtime error occurred\"))\n}\n\n```\n\n## References\n* OWASP: [Improper Error Handling](https://owasp.org/www-community/Improper_Error_Handling).\n* Common Weakness Enumeration: [CWE-209](https://cwe.mitre.org/data/definitions/209.html).\n* Common Weakness Enumeration: [CWE-497](https://cwe.mitre.org/data/definitions/497.html).\n", + "markdown" : "# Information exposure through a stack trace\nSoftware developers often add stack traces to error messages, as a debugging aid. Whenever that error message occurs for an end user, the developer can use the stack trace to help identify how to fix the problem. In particular, stack traces can tell the developer more about the sequence of events that led to a failure, as opposed to merely the final state of the software when the error occurred.\n\nUnfortunately, the same information can be useful to an attacker. The sequence of class names in a stack trace can reveal the structure of the application as well as any internal components it relies on.\n\n\n## Recommendation\nSend the user a more generic error message that reveals less information. Either suppress the stack trace entirely, or log it only on the server.\n\n\n## Example\nIn the following example, a panic is handled in two different ways. In the first version, labeled BAD, a detailed stack trace is written to a user-facing HTTP response object, which may disclose sensitive information. In the second version, the error message is logged only on the server. That way, the developers can still access and use the error log, but remote users will not see the information.\n\n\n```go\npackage example\n\nimport (\n\t\"log\"\n\t\"net/http\"\n\t\"runtime\"\n)\n\nfunc handlePanic(w http.ResponseWriter, r *http.Request) {\n\tbuf := make([]byte, 2<<16)\n\tbuf = buf[:runtime.Stack(buf, true)]\n\t// BAD: printing a stack trace back to the response\n\tw.Write(buf)\n\t// GOOD: logging the response to the server and sending\n\t// a more generic message.\n\tlog.Printf(\"Panic: %s\", buf)\n\tw.Write([]byte(\"An unexpected runtime error occurred\"))\n}\n\n```\n\n## References\n* OWASP: [Improper Error Handling](https://owasp.org/www-community/Improper_Error_Handling).\n* Common Weakness Enumeration: [CWE-209](https://cwe.mitre.org/data/definitions/209.html).\n* Common Weakness Enumeration: [CWE-497](https://cwe.mitre.org/data/definitions/497.html).\n" + }, + "properties" : { + "tags" : [ "security", "external/cwe/cwe-209", "external/cwe/cwe-497" ], + "description" : "Information from a stack trace propagates to an external user.\n Stack traces can unintentionally reveal implementation details\n that are useful to an attacker for developing a subsequent exploit.", + "id" : "go/stack-trace-exposure", + "kind" : "path-problem", + "name" : "Information exposure through a stack trace", + "precision" : "high", + "problem.severity" : "error", + "security-severity" : "5.4" + } + }, { + "id" : "go/insecure-randomness", + "name" : "go/insecure-randomness", + "shortDescription" : { + "text" : "Use of insufficient randomness as the key of a cryptographic algorithm" + }, + "fullDescription" : { + "text" : "Using insufficient randomness as the key of a cryptographic algorithm can allow an attacker to compromise security." + }, + "defaultConfiguration" : { + "enabled" : true, + "level" : "error" + }, + "help" : { + "text" : "# Use of insufficient randomness as the key of a cryptographic algorithm\nUsing a cryptographically weak pseudo-random number generator to generate a security-sensitive value, such as a password, makes it easier for an attacker to predict the value.\n\nPseudo-random number generators generate a sequence of numbers that only approximates the properties of random numbers. The sequence is not truly random because it is completely determined by a relatively small set of initial values, the seed. If the random number generator is cryptographically weak, then this sequence may be easily predictable through outside observations.\n\n\n## Recommendation\nUse a cryptographically secure pseudo-random number generator if the output is to be used in a security sensitive context. As a rule of thumb, a value should be considered \"security sensitive\" if predicting it would allow the attacker to perform an action that they would otherwise be unable to perform. For example, if an attacker could predict the random password generated for a new user, they would be able to log in as that new user.\n\nFor Go, `crypto/rand` provides a cryptographically secure pseudo-random number generator. `math/rand` is not cryptographically secure, and should be avoided in security contexts. For contexts which are not security sensitive, `math/rand` may be preferable as it has a more convenient interface, and is likely to be faster.\n\n\n## Example\nThe example below uses the `math/rand` package instead of `crypto/rand` to generate a password:\n\n\n```go\npackage main\n\nimport (\n\t\"math/rand\"\n)\n\nvar charset = []rune(\"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\")\n\nfunc generatePassword() string {\n\ts := make([]rune, 20)\n\tfor i := range s {\n\t\ts[i] = charset[rand.Intn(len(charset))]\n\t}\n\treturn string(s)\n}\n\n```\nInstead, use `crypto/rand`:\n\n\n```go\npackage main\n\nimport (\n\t\"crypto/rand\"\n\t\"math/big\"\n)\n\nfunc generatePasswordGood() string {\n\ts := make([]rune, 20)\n\tfor i := range s {\n\t\tidx, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))\n\t\tif err != nil {\n\t\t\t// handle err\n\t\t}\n\t\ts[i] = charset[idx.Int64()]\n\t}\n\treturn string(s)\n}\n\n```\n\n## References\n* Wikipedia. [Pseudo-random number generator](http://en.wikipedia.org/wiki/Pseudorandom_number_generator).\n* OWASP: [Insecure Randomness](https://owasp.org/www-community/vulnerabilities/Insecure_Randomness).\n* OWASP: [Secure Random Number Generation](https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html#secure-random-number-generation).\n* Common Weakness Enumeration: [CWE-338](https://cwe.mitre.org/data/definitions/338.html).\n", + "markdown" : "# Use of insufficient randomness as the key of a cryptographic algorithm\nUsing a cryptographically weak pseudo-random number generator to generate a security-sensitive value, such as a password, makes it easier for an attacker to predict the value.\n\nPseudo-random number generators generate a sequence of numbers that only approximates the properties of random numbers. The sequence is not truly random because it is completely determined by a relatively small set of initial values, the seed. If the random number generator is cryptographically weak, then this sequence may be easily predictable through outside observations.\n\n\n## Recommendation\nUse a cryptographically secure pseudo-random number generator if the output is to be used in a security sensitive context. As a rule of thumb, a value should be considered \"security sensitive\" if predicting it would allow the attacker to perform an action that they would otherwise be unable to perform. For example, if an attacker could predict the random password generated for a new user, they would be able to log in as that new user.\n\nFor Go, `crypto/rand` provides a cryptographically secure pseudo-random number generator. `math/rand` is not cryptographically secure, and should be avoided in security contexts. For contexts which are not security sensitive, `math/rand` may be preferable as it has a more convenient interface, and is likely to be faster.\n\n\n## Example\nThe example below uses the `math/rand` package instead of `crypto/rand` to generate a password:\n\n\n```go\npackage main\n\nimport (\n\t\"math/rand\"\n)\n\nvar charset = []rune(\"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\")\n\nfunc generatePassword() string {\n\ts := make([]rune, 20)\n\tfor i := range s {\n\t\ts[i] = charset[rand.Intn(len(charset))]\n\t}\n\treturn string(s)\n}\n\n```\nInstead, use `crypto/rand`:\n\n\n```go\npackage main\n\nimport (\n\t\"crypto/rand\"\n\t\"math/big\"\n)\n\nfunc generatePasswordGood() string {\n\ts := make([]rune, 20)\n\tfor i := range s {\n\t\tidx, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))\n\t\tif err != nil {\n\t\t\t// handle err\n\t\t}\n\t\ts[i] = charset[idx.Int64()]\n\t}\n\treturn string(s)\n}\n\n```\n\n## References\n* Wikipedia. [Pseudo-random number generator](http://en.wikipedia.org/wiki/Pseudorandom_number_generator).\n* OWASP: [Insecure Randomness](https://owasp.org/www-community/vulnerabilities/Insecure_Randomness).\n* OWASP: [Secure Random Number Generation](https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html#secure-random-number-generation).\n* Common Weakness Enumeration: [CWE-338](https://cwe.mitre.org/data/definitions/338.html).\n" + }, + "properties" : { + "tags" : [ "security", "external/cwe/cwe-338" ], + "description" : "Using insufficient randomness as the key of a cryptographic algorithm can allow an attacker to compromise security.", + "id" : "go/insecure-randomness", + "kind" : "path-problem", + "name" : "Use of insufficient randomness as the key of a cryptographic algorithm", + "precision" : "high", + "problem.severity" : "error", + "security-severity" : "7.8" + } + }, { + "id" : "go/path-injection", + "name" : "go/path-injection", + "shortDescription" : { + "text" : "Uncontrolled data used in path expression" + }, + "fullDescription" : { + "text" : "Accessing paths influenced by users can allow an attacker to access unexpected resources." + }, + "defaultConfiguration" : { + "enabled" : true, + "level" : "error" + }, + "help" : { + "text" : "# Uncontrolled data used in path expression\nAccessing files using paths constructed from user-controlled data can allow an attacker to access unexpected resources. This can result in sensitive information being revealed or deleted, or an attacker being able to influence behavior by modifying unexpected files.\n\n\n## Recommendation\nValidate user input before using it to construct a file path, either using an off-the-shelf library or by performing custom validation.\n\nIdeally, follow these rules:\n\n* Do not allow more than a single \".\" character.\n* Do not allow directory separators such as \"/\" or \"\\\\\" (depending on the file system).\n* Do not rely on simply replacing problematic sequences such as \"../\". For example, after applying this filter to \".../...//\", the resulting string would still be \"../\".\n* Use an allowlist of known good patterns.\n\n## Example\nIn the first example, a file name is read from an HTTP request and then used to access a file. However, a malicious user could enter a file name which is an absolute path, such as \"/etc/passwd\".\n\nIn the second example, it appears that the user is restricted to opening a file within the `\"user\"` home directory. However, a malicious user could enter a file name containing special characters. For example, the string `\"../../etc/passwd\"` will result in the code reading the file located at \"/home/user/../../etc/passwd\", which is the system's password file. This file would then be sent back to the user, giving them access to password information.\n\n\n```go\npackage main\n\nimport (\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"path/filepath\"\n)\n\nfunc handler(w http.ResponseWriter, r *http.Request) {\n\tpath := r.URL.Query()[\"path\"][0]\n\n\t// BAD: This could read any file on the file system\n\tdata, _ := ioutil.ReadFile(path)\n\tw.Write(data)\n\n\t// BAD: This could still read any file on the file system\n\tdata, _ = ioutil.ReadFile(filepath.Join(\"/home/user/\", path))\n\tw.Write(data)\n}\n\n```\n\n## References\n* OWASP: [Path Traversal](https://owasp.org/www-community/attacks/Path_Traversal).\n* Common Weakness Enumeration: [CWE-22](https://cwe.mitre.org/data/definitions/22.html).\n* Common Weakness Enumeration: [CWE-23](https://cwe.mitre.org/data/definitions/23.html).\n* Common Weakness Enumeration: [CWE-36](https://cwe.mitre.org/data/definitions/36.html).\n* Common Weakness Enumeration: [CWE-73](https://cwe.mitre.org/data/definitions/73.html).\n* Common Weakness Enumeration: [CWE-99](https://cwe.mitre.org/data/definitions/99.html).\n", + "markdown" : "# Uncontrolled data used in path expression\nAccessing files using paths constructed from user-controlled data can allow an attacker to access unexpected resources. This can result in sensitive information being revealed or deleted, or an attacker being able to influence behavior by modifying unexpected files.\n\n\n## Recommendation\nValidate user input before using it to construct a file path, either using an off-the-shelf library or by performing custom validation.\n\nIdeally, follow these rules:\n\n* Do not allow more than a single \".\" character.\n* Do not allow directory separators such as \"/\" or \"\\\\\" (depending on the file system).\n* Do not rely on simply replacing problematic sequences such as \"../\". For example, after applying this filter to \".../...//\", the resulting string would still be \"../\".\n* Use an allowlist of known good patterns.\n\n## Example\nIn the first example, a file name is read from an HTTP request and then used to access a file. However, a malicious user could enter a file name which is an absolute path, such as \"/etc/passwd\".\n\nIn the second example, it appears that the user is restricted to opening a file within the `\"user\"` home directory. However, a malicious user could enter a file name containing special characters. For example, the string `\"../../etc/passwd\"` will result in the code reading the file located at \"/home/user/../../etc/passwd\", which is the system's password file. This file would then be sent back to the user, giving them access to password information.\n\n\n```go\npackage main\n\nimport (\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"path/filepath\"\n)\n\nfunc handler(w http.ResponseWriter, r *http.Request) {\n\tpath := r.URL.Query()[\"path\"][0]\n\n\t// BAD: This could read any file on the file system\n\tdata, _ := ioutil.ReadFile(path)\n\tw.Write(data)\n\n\t// BAD: This could still read any file on the file system\n\tdata, _ = ioutil.ReadFile(filepath.Join(\"/home/user/\", path))\n\tw.Write(data)\n}\n\n```\n\n## References\n* OWASP: [Path Traversal](https://owasp.org/www-community/attacks/Path_Traversal).\n* Common Weakness Enumeration: [CWE-22](https://cwe.mitre.org/data/definitions/22.html).\n* Common Weakness Enumeration: [CWE-23](https://cwe.mitre.org/data/definitions/23.html).\n* Common Weakness Enumeration: [CWE-36](https://cwe.mitre.org/data/definitions/36.html).\n* Common Weakness Enumeration: [CWE-73](https://cwe.mitre.org/data/definitions/73.html).\n* Common Weakness Enumeration: [CWE-99](https://cwe.mitre.org/data/definitions/99.html).\n" + }, + "properties" : { + "tags" : [ "security", "external/cwe/cwe-022", "external/cwe/cwe-023", "external/cwe/cwe-036", "external/cwe/cwe-073", "external/cwe/cwe-099" ], + "description" : "Accessing paths influenced by users can allow an attacker to access\n unexpected resources.", + "id" : "go/path-injection", + "kind" : "path-problem", + "name" : "Uncontrolled data used in path expression", + "precision" : "high", + "problem.severity" : "error", + "security-severity" : "7.5" + } + }, { + "id" : "go/zipslip", + "name" : "go/zipslip", + "shortDescription" : { + "text" : "Arbitrary file access during archive extraction (\"Zip Slip\")" + }, + "fullDescription" : { + "text" : "Extracting files from a malicious ZIP file, or similar type of archive, without validating that the destination file path is within the destination directory can allow an attacker to unexpectedly gain access to resources." + }, + "defaultConfiguration" : { + "enabled" : true, + "level" : "error" + }, + "help" : { + "text" : "# Arbitrary file access during archive extraction (\"Zip Slip\")\nExtracting files from a malicious zip file, or similar type of archive, is at risk of directory traversal attacks if filenames from the archive are not properly validated. archive paths.\n\nZip archives contain archive entries representing each file in the archive. These entries include a file path for the entry, but these file paths are not restricted and may contain unexpected special elements such as the directory traversal element (`..`). If these file paths are used to create a filesystem path, then a file operation may happen in an unexpected location. This can result in sensitive information being revealed or deleted, or an attacker being able to influence behavior by modifying unexpected files.\n\nFor example, if a zip file contains a file entry `..\\sneaky-file`, and the zip file is extracted to the directory `c:\\output`, then naively combining the paths would result in an output file path of `c:\\output\\..\\sneaky-file`, which would cause the file to be written to `c:\\sneaky-file`.\n\n\n## Recommendation\nEnsure that output paths constructed from zip archive entries are validated to prevent writing files to unexpected locations.\n\nThe recommended way of writing an output file from a zip archive entry is to check that \"`..`\" does not occur in the path.\n\n\n## Example\nIn this example an archive is extracted without validating file paths. If `archive.zip` contained relative paths (for instance, if it were created by something like `zip archive.zip ../file.txt`) then executing this code could write to locations outside the destination directory.\n\n\n```go\npackage main\n\nimport (\n\t\"archive/zip\"\n\t\"io/ioutil\"\n\t\"path/filepath\"\n)\n\nfunc unzip(f string) {\n\tr, _ := zip.OpenReader(f)\n\tfor _, f := range r.File {\n\t\tp, _ := filepath.Abs(f.Name)\n\t\t// BAD: This could overwrite any file on the file system\n\t\tioutil.WriteFile(p, []byte(\"present\"), 0666)\n\t}\n}\n\n```\nTo fix this vulnerability, we need to check that the path does not contain any \"`..`\" elements in it.\n\n\n```go\npackage main\n\nimport (\n\t\"archive/zip\"\n\t\"io/ioutil\"\n\t\"path/filepath\"\n\t\"strings\"\n)\n\nfunc unzipGood(f string) {\n\tr, _ := zip.OpenReader(f)\n\tfor _, f := range r.File {\n\t\tp, _ := filepath.Abs(f.Name)\n\t\t// GOOD: Check that path does not contain \"..\" before using it\n\t\tif !strings.Contains(f.Name, \"..\") {\n\t\t\tioutil.WriteFile(p, []byte(\"present\"), 0666)\n\t\t}\n\t}\n}\n\n```\n\n## References\n* Snyk: [Zip Slip Vulnerability](https://snyk.io/research/zip-slip-vulnerability).\n* OWASP: [Path Traversal](https://owasp.org/www-community/attacks/Path_Traversal).\n* Common Weakness Enumeration: [CWE-22](https://cwe.mitre.org/data/definitions/22.html).\n", + "markdown" : "# Arbitrary file access during archive extraction (\"Zip Slip\")\nExtracting files from a malicious zip file, or similar type of archive, is at risk of directory traversal attacks if filenames from the archive are not properly validated. archive paths.\n\nZip archives contain archive entries representing each file in the archive. These entries include a file path for the entry, but these file paths are not restricted and may contain unexpected special elements such as the directory traversal element (`..`). If these file paths are used to create a filesystem path, then a file operation may happen in an unexpected location. This can result in sensitive information being revealed or deleted, or an attacker being able to influence behavior by modifying unexpected files.\n\nFor example, if a zip file contains a file entry `..\\sneaky-file`, and the zip file is extracted to the directory `c:\\output`, then naively combining the paths would result in an output file path of `c:\\output\\..\\sneaky-file`, which would cause the file to be written to `c:\\sneaky-file`.\n\n\n## Recommendation\nEnsure that output paths constructed from zip archive entries are validated to prevent writing files to unexpected locations.\n\nThe recommended way of writing an output file from a zip archive entry is to check that \"`..`\" does not occur in the path.\n\n\n## Example\nIn this example an archive is extracted without validating file paths. If `archive.zip` contained relative paths (for instance, if it were created by something like `zip archive.zip ../file.txt`) then executing this code could write to locations outside the destination directory.\n\n\n```go\npackage main\n\nimport (\n\t\"archive/zip\"\n\t\"io/ioutil\"\n\t\"path/filepath\"\n)\n\nfunc unzip(f string) {\n\tr, _ := zip.OpenReader(f)\n\tfor _, f := range r.File {\n\t\tp, _ := filepath.Abs(f.Name)\n\t\t// BAD: This could overwrite any file on the file system\n\t\tioutil.WriteFile(p, []byte(\"present\"), 0666)\n\t}\n}\n\n```\nTo fix this vulnerability, we need to check that the path does not contain any \"`..`\" elements in it.\n\n\n```go\npackage main\n\nimport (\n\t\"archive/zip\"\n\t\"io/ioutil\"\n\t\"path/filepath\"\n\t\"strings\"\n)\n\nfunc unzipGood(f string) {\n\tr, _ := zip.OpenReader(f)\n\tfor _, f := range r.File {\n\t\tp, _ := filepath.Abs(f.Name)\n\t\t// GOOD: Check that path does not contain \"..\" before using it\n\t\tif !strings.Contains(f.Name, \"..\") {\n\t\t\tioutil.WriteFile(p, []byte(\"present\"), 0666)\n\t\t}\n\t}\n}\n\n```\n\n## References\n* Snyk: [Zip Slip Vulnerability](https://snyk.io/research/zip-slip-vulnerability).\n* OWASP: [Path Traversal](https://owasp.org/www-community/attacks/Path_Traversal).\n* Common Weakness Enumeration: [CWE-22](https://cwe.mitre.org/data/definitions/22.html).\n" + }, + "properties" : { + "tags" : [ "security", "external/cwe/cwe-022" ], + "description" : "Extracting files from a malicious ZIP file, or similar type of archive, without\n validating that the destination file path is within the destination directory\n can allow an attacker to unexpectedly gain access to resources.", + "id" : "go/zipslip", + "kind" : "path-problem", + "name" : "Arbitrary file access during archive extraction (\"Zip Slip\")", + "precision" : "high", + "problem.severity" : "error", + "security-severity" : "7.5" + } + }, { + "id" : "go/unsafe-unzip-symlink", + "name" : "go/unsafe-unzip-symlink", + "shortDescription" : { + "text" : "Arbitrary file write extracting an archive containing symbolic links" + }, + "fullDescription" : { + "text" : "Extracting files from a malicious zip archive without validating that the destination file path is within the destination directory can cause files outside the destination directory to be overwritten. Extracting symbolic links in particular requires resolving previously extracted links to ensure the destination directory is not escaped." + }, + "defaultConfiguration" : { + "enabled" : true, + "level" : "error" + }, + "help" : { + "text" : "# Arbitrary file write extracting an archive containing symbolic links\nExtracting symbolic links from a malicious zip archive, without validating that the destination file path is within the destination directory, can cause files outside the destination directory to be overwritten. This can happen if there are previously-extracted symbolic links or directory traversal elements and links (`..`) in archive paths.\n\nThis problem is related to the ZipSlip vulnerability which is detected by the `go/zipslip` query; please see that query's help for more general information about malicious archive file vulnerabilities. This query considers the specific case where symbolic links are extracted from an archive, in which case the extraction code must be aware of existing symbolic links when checking whether it is about to extract a link pointing to a location outside the target extraction directory.\n\n\n## Recommendation\nEnsure that output paths constructed from zip archive entries are validated. This includes resolving any previously extracted symbolic links, for example using `path/filepath.EvalSymlinks`, to prevent writing files or links to unexpected locations.\n\n\n## Example\nIn this example, links are extracted from an archive using the syntactic `filepath.Rel` function to check whether the link and its target fall within the destination directory. However, the extraction code doesn't resolve previously-extracted links, so a pair of links like `subdir/parent -> ..` followed by `escape -> subdir/parent/.. -> subdir/../..` leaves a link pointing to the parent of the archive root. The syntactic `Rel` is ineffective because it equates `subdir/parent/..` with `subdir/`, but this is not the case when `subdir/parent` is a symbolic link.\n\n\n```go\npackage main\n\nimport (\n\t\"archive/tar\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n)\n\nfunc isRel(candidate, target string) bool {\n\t// BAD: purely syntactic means are used to check\n\t// that `candidate` does not escape from `target`\n\tif filepath.IsAbs(candidate) {\n\t\treturn false\n\t}\n\trelpath, err := filepath.Rel(target, filepath.Join(target, candidate))\n\treturn err == nil && !strings.HasPrefix(filepath.Clean(relpath), \"..\")\n}\n\nfunc unzipSymlink(f io.Reader, target string) {\n\tr := tar.NewReader(f)\n\tfor {\n\t\theader, err := r.Next()\n\t\tif err != nil {\n\t\t\tbreak\n\t\t}\n\t\tif isRel(header.Linkname, target) && isRel(header.Name, target) {\n\t\t\tos.Symlink(header.Linkname, header.Name)\n\t\t}\n\t}\n}\n\n```\nTo fix this vulnerability, resolve pre-existing symbolic links before checking that the link's target is acceptable:\n\n\n```go\npackage main\n\nfunc isRel(candidate, target string) bool {\n\t// GOOD: resolves all symbolic links before checking\n\t// that `candidate` does not escape from `target`\n\tif filepath.IsAbs(candidate) {\n\t\treturn false\n\t}\n\trealpath, err := filepath.EvalSymlinks(filepath.Join(target, candidate))\n\tif err != nil {\n\t\treturn false\n\t}\n\trelpath, err := filepath.Rel(target, realpath)\n\treturn err == nil && !strings.HasPrefix(filepath.Clean(relpath), \"..\")\n}\n\n```\n\n## References\n* Snyk: [Zip Slip Vulnerability](https://snyk.io/research/zip-slip-vulnerability).\n* OWASP: [Path Traversal](https://owasp.org/www-community/attacks/Path_Traversal).\n* Common Weakness Enumeration: [CWE-22](https://cwe.mitre.org/data/definitions/22.html).\n", + "markdown" : "# Arbitrary file write extracting an archive containing symbolic links\nExtracting symbolic links from a malicious zip archive, without validating that the destination file path is within the destination directory, can cause files outside the destination directory to be overwritten. This can happen if there are previously-extracted symbolic links or directory traversal elements and links (`..`) in archive paths.\n\nThis problem is related to the ZipSlip vulnerability which is detected by the `go/zipslip` query; please see that query's help for more general information about malicious archive file vulnerabilities. This query considers the specific case where symbolic links are extracted from an archive, in which case the extraction code must be aware of existing symbolic links when checking whether it is about to extract a link pointing to a location outside the target extraction directory.\n\n\n## Recommendation\nEnsure that output paths constructed from zip archive entries are validated. This includes resolving any previously extracted symbolic links, for example using `path/filepath.EvalSymlinks`, to prevent writing files or links to unexpected locations.\n\n\n## Example\nIn this example, links are extracted from an archive using the syntactic `filepath.Rel` function to check whether the link and its target fall within the destination directory. However, the extraction code doesn't resolve previously-extracted links, so a pair of links like `subdir/parent -> ..` followed by `escape -> subdir/parent/.. -> subdir/../..` leaves a link pointing to the parent of the archive root. The syntactic `Rel` is ineffective because it equates `subdir/parent/..` with `subdir/`, but this is not the case when `subdir/parent` is a symbolic link.\n\n\n```go\npackage main\n\nimport (\n\t\"archive/tar\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n)\n\nfunc isRel(candidate, target string) bool {\n\t// BAD: purely syntactic means are used to check\n\t// that `candidate` does not escape from `target`\n\tif filepath.IsAbs(candidate) {\n\t\treturn false\n\t}\n\trelpath, err := filepath.Rel(target, filepath.Join(target, candidate))\n\treturn err == nil && !strings.HasPrefix(filepath.Clean(relpath), \"..\")\n}\n\nfunc unzipSymlink(f io.Reader, target string) {\n\tr := tar.NewReader(f)\n\tfor {\n\t\theader, err := r.Next()\n\t\tif err != nil {\n\t\t\tbreak\n\t\t}\n\t\tif isRel(header.Linkname, target) && isRel(header.Name, target) {\n\t\t\tos.Symlink(header.Linkname, header.Name)\n\t\t}\n\t}\n}\n\n```\nTo fix this vulnerability, resolve pre-existing symbolic links before checking that the link's target is acceptable:\n\n\n```go\npackage main\n\nfunc isRel(candidate, target string) bool {\n\t// GOOD: resolves all symbolic links before checking\n\t// that `candidate` does not escape from `target`\n\tif filepath.IsAbs(candidate) {\n\t\treturn false\n\t}\n\trealpath, err := filepath.EvalSymlinks(filepath.Join(target, candidate))\n\tif err != nil {\n\t\treturn false\n\t}\n\trelpath, err := filepath.Rel(target, realpath)\n\treturn err == nil && !strings.HasPrefix(filepath.Clean(relpath), \"..\")\n}\n\n```\n\n## References\n* Snyk: [Zip Slip Vulnerability](https://snyk.io/research/zip-slip-vulnerability).\n* OWASP: [Path Traversal](https://owasp.org/www-community/attacks/Path_Traversal).\n* Common Weakness Enumeration: [CWE-22](https://cwe.mitre.org/data/definitions/22.html).\n" + }, + "properties" : { + "tags" : [ "security", "external/cwe/cwe-022" ], + "description" : "Extracting files from a malicious zip archive without validating that the\n destination file path is within the destination directory can cause files outside\n the destination directory to be overwritten. Extracting symbolic links in particular\n requires resolving previously extracted links to ensure the destination directory\n is not escaped.", + "id" : "go/unsafe-unzip-symlink", + "kind" : "path-problem", + "name" : "Arbitrary file write extracting an archive containing symbolic links", + "precision" : "high", + "problem.severity" : "error", + "security-severity" : "7.5" + } + }, { + "id" : "go/insecure-hostkeycallback", + "name" : "go/insecure-hostkeycallback", + "shortDescription" : { + "text" : "Use of insecure HostKeyCallback implementation" + }, + "fullDescription" : { + "text" : "Detects insecure SSL client configurations with an implementation of the `HostKeyCallback` that accepts all host keys." + }, + "defaultConfiguration" : { + "enabled" : true, + "level" : "warning" + }, + "help" : { + "text" : "# Use of insecure HostKeyCallback implementation\nThe `ClientConfig` specifying the configuration for establishing a SSH connection has a field `HostKeyCallback` that must be initialized with a function that validates the host key returned by the server.\n\nNot properly verifying the host key returned by a server provides attackers with an opportunity to perform a Machine-in-the-Middle (MitM) attack. A successful attack can compromise the confidentiality and integrity of the information communicated with the server.\n\nThe `ssh` package provides the predefined callback `InsecureIgnoreHostKey` that can be used during development and testing. It accepts any provided host key. This callback, or a semantically similar callback, should not be used in production code.\n\n\n## Recommendation\nThe `HostKeyCallback` field of `ClientConfig` should be initialized with a function that validates a host key against an allow list. If a key is not on a predefined allow list, the connection must be terminated and the failed security operation should be logged.\n\nWhen the allow list contains only a single host key then the function `FixedHostKey` can be used.\n\n\n## Example\nThe following example shows the use of `InsecureIgnoreHostKey` and an insecure host key callback implementation commonly used in non-production code.\n\n\n```go\npackage main\n\nimport (\n\t\"golang.org/x/crypto/ssh\"\n\t\"net\"\n)\n\nfunc main() {}\n\nfunc insecureIgnoreHostKey() {\n\t_ = &ssh.ClientConfig{\n\t\tUser: \"username\",\n\t\tAuth: []ssh.AuthMethod{nil},\n\t\tHostKeyCallback: ssh.InsecureIgnoreHostKey(),\n\t}\n}\n\nfunc insecureHostKeyCallback() {\n\t_ = &ssh.ClientConfig{\n\t\tUser: \"username\",\n\t\tAuth: []ssh.AuthMethod{nil},\n\t\tHostKeyCallback: ssh.HostKeyCallback(\n\t\t\tfunc(hostname string, remote net.Addr, key ssh.PublicKey) error {\n\t\t\t\treturn nil\n\t\t\t}),\n\t}\n}\n\n```\nThe next example shows a secure implementation using the `FixedHostKey` that implements an allow-list.\n\n\n```go\npackage main\n\nimport (\n\t\"golang.org/x/crypto/ssh\"\n\t\"io/ioutil\"\n)\n\nfunc main() {}\n\nfunc secureHostKeyCallback() {\n\tpublicKeyBytes, _ := ioutil.ReadFile(\"allowed_hostkey.pub\")\n\tpublicKey, _ := ssh.ParsePublicKey(publicKeyBytes)\n\n\t_ = &ssh.ClientConfig{\n\t\tUser: \"username\",\n\t\tAuth: []ssh.AuthMethod{nil},\n\t\tHostKeyCallback: ssh.FixedHostKey(publicKey),\n\t}\n}\n\n```\n\n## References\n* Go Dev: [package ssh](https://pkg.go.dev/golang.org/x/crypto/ssh?tab=doc).\n* Common Weakness Enumeration: [CWE-322](https://cwe.mitre.org/data/definitions/322.html).\n", + "markdown" : "# Use of insecure HostKeyCallback implementation\nThe `ClientConfig` specifying the configuration for establishing a SSH connection has a field `HostKeyCallback` that must be initialized with a function that validates the host key returned by the server.\n\nNot properly verifying the host key returned by a server provides attackers with an opportunity to perform a Machine-in-the-Middle (MitM) attack. A successful attack can compromise the confidentiality and integrity of the information communicated with the server.\n\nThe `ssh` package provides the predefined callback `InsecureIgnoreHostKey` that can be used during development and testing. It accepts any provided host key. This callback, or a semantically similar callback, should not be used in production code.\n\n\n## Recommendation\nThe `HostKeyCallback` field of `ClientConfig` should be initialized with a function that validates a host key against an allow list. If a key is not on a predefined allow list, the connection must be terminated and the failed security operation should be logged.\n\nWhen the allow list contains only a single host key then the function `FixedHostKey` can be used.\n\n\n## Example\nThe following example shows the use of `InsecureIgnoreHostKey` and an insecure host key callback implementation commonly used in non-production code.\n\n\n```go\npackage main\n\nimport (\n\t\"golang.org/x/crypto/ssh\"\n\t\"net\"\n)\n\nfunc main() {}\n\nfunc insecureIgnoreHostKey() {\n\t_ = &ssh.ClientConfig{\n\t\tUser: \"username\",\n\t\tAuth: []ssh.AuthMethod{nil},\n\t\tHostKeyCallback: ssh.InsecureIgnoreHostKey(),\n\t}\n}\n\nfunc insecureHostKeyCallback() {\n\t_ = &ssh.ClientConfig{\n\t\tUser: \"username\",\n\t\tAuth: []ssh.AuthMethod{nil},\n\t\tHostKeyCallback: ssh.HostKeyCallback(\n\t\t\tfunc(hostname string, remote net.Addr, key ssh.PublicKey) error {\n\t\t\t\treturn nil\n\t\t\t}),\n\t}\n}\n\n```\nThe next example shows a secure implementation using the `FixedHostKey` that implements an allow-list.\n\n\n```go\npackage main\n\nimport (\n\t\"golang.org/x/crypto/ssh\"\n\t\"io/ioutil\"\n)\n\nfunc main() {}\n\nfunc secureHostKeyCallback() {\n\tpublicKeyBytes, _ := ioutil.ReadFile(\"allowed_hostkey.pub\")\n\tpublicKey, _ := ssh.ParsePublicKey(publicKeyBytes)\n\n\t_ = &ssh.ClientConfig{\n\t\tUser: \"username\",\n\t\tAuth: []ssh.AuthMethod{nil},\n\t\tHostKeyCallback: ssh.FixedHostKey(publicKey),\n\t}\n}\n\n```\n\n## References\n* Go Dev: [package ssh](https://pkg.go.dev/golang.org/x/crypto/ssh?tab=doc).\n* Common Weakness Enumeration: [CWE-322](https://cwe.mitre.org/data/definitions/322.html).\n" + }, + "properties" : { + "tags" : [ "security", "external/cwe/cwe-322" ], + "description" : "Detects insecure SSL client configurations with an implementation of the `HostKeyCallback` that accepts all host keys.", + "id" : "go/insecure-hostkeycallback", + "kind" : "path-problem", + "name" : "Use of insecure HostKeyCallback implementation", + "precision" : "high", + "problem.severity" : "warning", + "security-severity" : "8.2" + } + }, { + "id" : "go/allocation-size-overflow", + "name" : "go/allocation-size-overflow", + "shortDescription" : { + "text" : "Size computation for allocation may overflow" + }, + "fullDescription" : { + "text" : "When computing the size of an allocation based on the size of a large object, the result may overflow and cause a runtime panic." + }, + "defaultConfiguration" : { + "enabled" : true, + "level" : "warning" + }, + "help" : { + "text" : "# Size computation for allocation may overflow\nPerforming calculations involving the size of potentially large strings or slices can result in an overflow (for signed integer types) or a wraparound (for unsigned types). An overflow causes the result of the calculation to become negative, while a wraparound results in a small (positive) number.\n\nThis can cause further issues. If, for example, the result is then used in an allocation, it will cause a runtime panic if it is negative, and allocate an unexpectedly small buffer otherwise.\n\n\n## Recommendation\nAlways guard against overflow in arithmetic operations involving potentially large numbers by doing one of the following:\n\n* Validate the size of the data from which the numbers are computed.\n* Define a guard on the arithmetic expression, so that the operation is performed only if the result can be known to be less than, or equal to, the maximum value for the type.\n* Use a wider type (such as `uint64` instead of `int`), so that larger input values do not cause overflow.\n\n## Example\nIn the following example, assume that there is a function `encryptBuffer` that encrypts byte slices whose length must be padded to be a multiple of 16. The function `encryptValue` provides a convenience wrapper around this function: when passed an arbitrary value, it first encodes that value as JSON, pads the resulting byte slice, and then passes it to `encryptBuffer`.\n\n\n```go\npackage main\n\nimport \"encoding/json\"\n\nfunc encryptValue(v interface{}) ([]byte, error) {\n\tjsonData, err := json.Marshal(v)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tsize := len(jsonData) + (len(jsonData) % 16)\n\tbuffer := make([]byte, size)\n\tcopy(buffer, jsonData)\n\treturn encryptBuffer(buffer)\n}\n\n```\nWhen passed a value whose JSON encoding is close to the maximum value of type `int` in length, the computation of `size` will overflow, producing a negative value. When that negative value is passed to `make`, a runtime panic will occur.\n\nTo guard against this, the function should be improved to check the length of the JSON-encoded value. For example, here is a version of `encryptValue` that ensures the value is no larger than 64 MB, which fits comfortably within an `int` and avoids the overflow:\n\n\n```go\npackage main\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n)\n\nfunc encryptValueGood(v interface{}) ([]byte, error) {\n\tjsonData, err := json.Marshal(v)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(jsonData) > 64*1024*1024 {\n\t\treturn nil, errors.New(\"value too large\")\n\t}\n\tsize := len(jsonData) + (len(jsonData) % 16)\n\tbuffer := make([]byte, size)\n\tcopy(buffer, jsonData)\n\treturn encryptBuffer(buffer)\n}\n\n```\n\n## References\n* The Go Programming Language Specification: [Integer overflow](https://golang.org/ref/spec#Integer_overflow).\n* The Go Programming Language Specification: [Making slices, maps and channels](https://golang.org/ref/spec#Making_slices_maps_and_channels).\n* Common Weakness Enumeration: [CWE-190](https://cwe.mitre.org/data/definitions/190.html).\n", + "markdown" : "# Size computation for allocation may overflow\nPerforming calculations involving the size of potentially large strings or slices can result in an overflow (for signed integer types) or a wraparound (for unsigned types). An overflow causes the result of the calculation to become negative, while a wraparound results in a small (positive) number.\n\nThis can cause further issues. If, for example, the result is then used in an allocation, it will cause a runtime panic if it is negative, and allocate an unexpectedly small buffer otherwise.\n\n\n## Recommendation\nAlways guard against overflow in arithmetic operations involving potentially large numbers by doing one of the following:\n\n* Validate the size of the data from which the numbers are computed.\n* Define a guard on the arithmetic expression, so that the operation is performed only if the result can be known to be less than, or equal to, the maximum value for the type.\n* Use a wider type (such as `uint64` instead of `int`), so that larger input values do not cause overflow.\n\n## Example\nIn the following example, assume that there is a function `encryptBuffer` that encrypts byte slices whose length must be padded to be a multiple of 16. The function `encryptValue` provides a convenience wrapper around this function: when passed an arbitrary value, it first encodes that value as JSON, pads the resulting byte slice, and then passes it to `encryptBuffer`.\n\n\n```go\npackage main\n\nimport \"encoding/json\"\n\nfunc encryptValue(v interface{}) ([]byte, error) {\n\tjsonData, err := json.Marshal(v)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tsize := len(jsonData) + (len(jsonData) % 16)\n\tbuffer := make([]byte, size)\n\tcopy(buffer, jsonData)\n\treturn encryptBuffer(buffer)\n}\n\n```\nWhen passed a value whose JSON encoding is close to the maximum value of type `int` in length, the computation of `size` will overflow, producing a negative value. When that negative value is passed to `make`, a runtime panic will occur.\n\nTo guard against this, the function should be improved to check the length of the JSON-encoded value. For example, here is a version of `encryptValue` that ensures the value is no larger than 64 MB, which fits comfortably within an `int` and avoids the overflow:\n\n\n```go\npackage main\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n)\n\nfunc encryptValueGood(v interface{}) ([]byte, error) {\n\tjsonData, err := json.Marshal(v)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(jsonData) > 64*1024*1024 {\n\t\treturn nil, errors.New(\"value too large\")\n\t}\n\tsize := len(jsonData) + (len(jsonData) % 16)\n\tbuffer := make([]byte, size)\n\tcopy(buffer, jsonData)\n\treturn encryptBuffer(buffer)\n}\n\n```\n\n## References\n* The Go Programming Language Specification: [Integer overflow](https://golang.org/ref/spec#Integer_overflow).\n* The Go Programming Language Specification: [Making slices, maps and channels](https://golang.org/ref/spec#Making_slices_maps_and_channels).\n* Common Weakness Enumeration: [CWE-190](https://cwe.mitre.org/data/definitions/190.html).\n" + }, + "properties" : { + "tags" : [ "security", "external/cwe/cwe-190" ], + "description" : "When computing the size of an allocation based on the size of a large object,\n the result may overflow and cause a runtime panic.", + "id" : "go/allocation-size-overflow", + "kind" : "path-problem", + "name" : "Size computation for allocation may overflow", + "precision" : "high", + "problem.severity" : "warning", + "security-severity" : "8.1" + } + }, { + "id" : "go/command-injection", + "name" : "go/command-injection", + "shortDescription" : { + "text" : "Command built from user-controlled sources" + }, + "fullDescription" : { + "text" : "Building a system command from user-controlled sources is vulnerable to insertion of malicious code by the user." + }, + "defaultConfiguration" : { + "enabled" : true, + "level" : "error" + }, + "help" : { + "text" : "# Command built from user-controlled sources\nIf a system command invocation is built from user-provided data without sufficient sanitization, a malicious user may be able to run commands to exfiltrate data or compromise the system.\n\n\n## Recommendation\nIf possible, use hard-coded string literals to specify the command to run. Instead of interpreting user input directly as command names, examine the input and then choose among hard-coded string literals.\n\nIf this is not possible, then add sanitization code to verify that the user input is safe before using it.\n\n\n## Example\nIn the following example, assume the function `handler` is an HTTP request handler in a web application, whose parameter `req` contains the request object:\n\n\n```go\npackage main\n\nimport (\n\t\"net/http\"\n\t\"os/exec\"\n)\n\nfunc handler(req *http.Request) {\n\tcmdName := req.URL.Query()[\"cmd\"][0]\n\tcmd := exec.Command(cmdName)\n\tcmd.Run()\n}\n\n```\nThe handler extracts the name of a system command from the request object, and then runs it without any further checks, which can cause a command-injection vulnerability.\n\n\n## References\n* OWASP: [Command Injection](https://www.owasp.org/index.php/Command_Injection).\n* Common Weakness Enumeration: [CWE-78](https://cwe.mitre.org/data/definitions/78.html).\n", + "markdown" : "# Command built from user-controlled sources\nIf a system command invocation is built from user-provided data without sufficient sanitization, a malicious user may be able to run commands to exfiltrate data or compromise the system.\n\n\n## Recommendation\nIf possible, use hard-coded string literals to specify the command to run. Instead of interpreting user input directly as command names, examine the input and then choose among hard-coded string literals.\n\nIf this is not possible, then add sanitization code to verify that the user input is safe before using it.\n\n\n## Example\nIn the following example, assume the function `handler` is an HTTP request handler in a web application, whose parameter `req` contains the request object:\n\n\n```go\npackage main\n\nimport (\n\t\"net/http\"\n\t\"os/exec\"\n)\n\nfunc handler(req *http.Request) {\n\tcmdName := req.URL.Query()[\"cmd\"][0]\n\tcmd := exec.Command(cmdName)\n\tcmd.Run()\n}\n\n```\nThe handler extracts the name of a system command from the request object, and then runs it without any further checks, which can cause a command-injection vulnerability.\n\n\n## References\n* OWASP: [Command Injection](https://www.owasp.org/index.php/Command_Injection).\n* Common Weakness Enumeration: [CWE-78](https://cwe.mitre.org/data/definitions/78.html).\n" + }, + "properties" : { + "tags" : [ "security", "external/cwe/cwe-078" ], + "description" : "Building a system command from user-controlled sources is vulnerable to insertion of\n malicious code by the user.", + "id" : "go/command-injection", + "kind" : "path-problem", + "name" : "Command built from user-controlled sources", + "precision" : "high", + "problem.severity" : "error", + "security-severity" : "9.8" + } + }, { + "id" : "go/weak-crypto-key", + "name" : "go/weak-crypto-key", + "shortDescription" : { + "text" : "Use of a weak cryptographic key" + }, + "fullDescription" : { + "text" : "Using a weak cryptographic key can allow an attacker to compromise security." + }, + "defaultConfiguration" : { + "enabled" : true, + "level" : "error" + }, + "help" : { + "text" : "# Use of a weak cryptographic key\nIncorrect uses of encryption algorithms may result in sensitive data exposure, key leakage, broken authentication, insecure session, and spoofing attacks.\n\n\n## Recommendation\nEnsure that you use a strong key with a recommended bit size. For RSA encryption the minimum size is 2048 bits.\n\n\n## Example\nThe following code uses RSA encryption with insufficient key size.\n\n\n```go\npackage main\n\nimport (\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"fmt\"\n)\n\nfunc main() {\n\t//Generate Private Key\n\tpvk, err := rsa.GenerateKey(rand.Reader, 1024)\n\tif err != nil {\n\t\tfmt.Println(err)\n\t}\n\tfmt.Println(pvk)\n}\n\n```\nIn the example below, the key size is set to 2048 bits.\n\n\n```go\npackage main\n\nimport (\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"fmt\"\n)\n\nfunc main() {\n\t//Generate Private Key\n\tpvk, err := rsa.GenerateKey(rand.Reader, 2048)\n\tif err != nil {\n\t\tfmt.Println(err)\n\t}\n\tfmt.Println(pvk)\n}\n\n```\n\n## References\n* OWASP: [Cryptographic Storage Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html).\n* Wikipedia: [Cryptographically Strong Algorithms](https://en.wikipedia.org/wiki/Strong_cryptography#Cryptographically_strong_algorithms).\n* Wikipedia: [Strong Cryptography Examples](https://en.wikipedia.org/wiki/Strong_cryptography#Examples).\n* NIST, FIPS 140 Annex a: [ Approved Security Functions](http://csrc.nist.gov/publications/fips/fips140-2/fips1402annexa.pdf).\n* NIST, SP 800-131A: [ Transitions: Recommendation for Transitioning the Use of Cryptographic Algorithms and Key Lengths](http://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-131Ar1.pdf).\n* Common Weakness Enumeration: [CWE-326](https://cwe.mitre.org/data/definitions/326.html).\n", + "markdown" : "# Use of a weak cryptographic key\nIncorrect uses of encryption algorithms may result in sensitive data exposure, key leakage, broken authentication, insecure session, and spoofing attacks.\n\n\n## Recommendation\nEnsure that you use a strong key with a recommended bit size. For RSA encryption the minimum size is 2048 bits.\n\n\n## Example\nThe following code uses RSA encryption with insufficient key size.\n\n\n```go\npackage main\n\nimport (\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"fmt\"\n)\n\nfunc main() {\n\t//Generate Private Key\n\tpvk, err := rsa.GenerateKey(rand.Reader, 1024)\n\tif err != nil {\n\t\tfmt.Println(err)\n\t}\n\tfmt.Println(pvk)\n}\n\n```\nIn the example below, the key size is set to 2048 bits.\n\n\n```go\npackage main\n\nimport (\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"fmt\"\n)\n\nfunc main() {\n\t//Generate Private Key\n\tpvk, err := rsa.GenerateKey(rand.Reader, 2048)\n\tif err != nil {\n\t\tfmt.Println(err)\n\t}\n\tfmt.Println(pvk)\n}\n\n```\n\n## References\n* OWASP: [Cryptographic Storage Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html).\n* Wikipedia: [Cryptographically Strong Algorithms](https://en.wikipedia.org/wiki/Strong_cryptography#Cryptographically_strong_algorithms).\n* Wikipedia: [Strong Cryptography Examples](https://en.wikipedia.org/wiki/Strong_cryptography#Examples).\n* NIST, FIPS 140 Annex a: [ Approved Security Functions](http://csrc.nist.gov/publications/fips/fips140-2/fips1402annexa.pdf).\n* NIST, SP 800-131A: [ Transitions: Recommendation for Transitioning the Use of Cryptographic Algorithms and Key Lengths](http://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-131Ar1.pdf).\n* Common Weakness Enumeration: [CWE-326](https://cwe.mitre.org/data/definitions/326.html).\n" + }, + "properties" : { + "tags" : [ "security", "external/cwe/cwe-326" ], + "description" : "Using a weak cryptographic key can allow an attacker to compromise security.", + "id" : "go/weak-crypto-key", + "kind" : "path-problem", + "name" : "Use of a weak cryptographic key", + "precision" : "high", + "problem.severity" : "error", + "security-severity" : "7.5" + } + }, { + "id" : "go/insecure-tls", + "name" : "go/insecure-tls", + "shortDescription" : { + "text" : "Insecure TLS configuration" + }, + "fullDescription" : { + "text" : "If an application supports insecure TLS versions or ciphers, it may be vulnerable to machine-in-the-middle and other attacks." + }, + "defaultConfiguration" : { + "enabled" : true, + "level" : "warning" + }, + "help" : { + "text" : "# Insecure TLS configuration\nThe TLS (Transport Layer Security) protocol secures communications over the Internet. The protocol allows client/server applications to communicate in a way that is designed to prevent eavesdropping, tampering, or message forgery.\n\nThe current latest version is 1.3 (with the 1.2 version still being considered secure). Older versions are not deemed to be secure anymore because of various security vulnerabilities, and tht makes them unfit for use in securing your applications.\n\nUnfortunately, many applications and websites still support deprecated SSL/TLS versions and cipher suites.\n\n\n## Recommendation\nOnly use secure TLS versions (1.3 and 1.2) and avoid using insecure cipher suites (you can see a list here: https://golang.org/src/crypto/tls/cipher_suites.go\\#L81)\n\n\n## Example\nThe following example shows a few ways how an insecure TLS configuration can be created:\n\n\n```go\npackage main\n\nimport (\n\t\"crypto/tls\"\n)\n\nfunc main() {}\n\nfunc insecureMinMaxTlsVersion() {\n\t{\n\t\tconfig := &tls.Config{}\n\t\tconfig.MinVersion = 0 // BAD: Setting the MinVersion to 0 equals to choosing the lowest supported version (i.e. SSL3.0)\n\t}\n\t{\n\t\tconfig := &tls.Config{}\n\t\tconfig.MinVersion = tls.VersionSSL30 // BAD: SSL 3.0 is a non-secure version of the protocol; it's not safe to use it as MinVersion.\n\t}\n\t{\n\t\tconfig := &tls.Config{}\n\t\tconfig.MaxVersion = tls.VersionSSL30 // BAD: SSL 3.0 is a non-secure version of the protocol; it's not safe to use it as MaxVersion.\n\t}\n}\n\nfunc insecureCipherSuites() {\n\tconfig := &tls.Config{\n\t\tCipherSuites: []uint16{\n\t\t\ttls.TLS_RSA_WITH_RC4_128_SHA, // BAD: TLS_RSA_WITH_RC4_128_SHA is one of the non-secure cipher suites; it's not safe to be used.\n\t\t},\n\t}\n\t_ = config\n}\n\n```\nThe following example shows how to create a safer TLS configuration:\n\n\n```go\npackage main\n\nimport \"crypto/tls\"\n\nfunc saferTLSConfig() {\n\tconfig := &tls.Config{}\n\tconfig.MinVersion = tls.VersionTLS12\n\tconfig.MaxVersion = tls.VersionTLS13\n\t// OR\n\tconfig.MaxVersion = 0 // GOOD: Setting MaxVersion to 0 means that the highest version available in the package will be used.\n}\n\n```\n\n## References\n* Wikipedia: [Transport Layer Security](https://en.wikipedia.org/wiki/Transport_Layer_Security)\n* Mozilla: [Security/Server Side TLS](https://wiki.mozilla.org/Security/Server_Side_TLS)\n* OWASP: [Transport Layer Protection Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Transport_Layer_Protection_Cheat_Sheet.html)\n* Common Weakness Enumeration: [CWE-327](https://cwe.mitre.org/data/definitions/327.html).\n", + "markdown" : "# Insecure TLS configuration\nThe TLS (Transport Layer Security) protocol secures communications over the Internet. The protocol allows client/server applications to communicate in a way that is designed to prevent eavesdropping, tampering, or message forgery.\n\nThe current latest version is 1.3 (with the 1.2 version still being considered secure). Older versions are not deemed to be secure anymore because of various security vulnerabilities, and tht makes them unfit for use in securing your applications.\n\nUnfortunately, many applications and websites still support deprecated SSL/TLS versions and cipher suites.\n\n\n## Recommendation\nOnly use secure TLS versions (1.3 and 1.2) and avoid using insecure cipher suites (you can see a list here: https://golang.org/src/crypto/tls/cipher_suites.go\\#L81)\n\n\n## Example\nThe following example shows a few ways how an insecure TLS configuration can be created:\n\n\n```go\npackage main\n\nimport (\n\t\"crypto/tls\"\n)\n\nfunc main() {}\n\nfunc insecureMinMaxTlsVersion() {\n\t{\n\t\tconfig := &tls.Config{}\n\t\tconfig.MinVersion = 0 // BAD: Setting the MinVersion to 0 equals to choosing the lowest supported version (i.e. SSL3.0)\n\t}\n\t{\n\t\tconfig := &tls.Config{}\n\t\tconfig.MinVersion = tls.VersionSSL30 // BAD: SSL 3.0 is a non-secure version of the protocol; it's not safe to use it as MinVersion.\n\t}\n\t{\n\t\tconfig := &tls.Config{}\n\t\tconfig.MaxVersion = tls.VersionSSL30 // BAD: SSL 3.0 is a non-secure version of the protocol; it's not safe to use it as MaxVersion.\n\t}\n}\n\nfunc insecureCipherSuites() {\n\tconfig := &tls.Config{\n\t\tCipherSuites: []uint16{\n\t\t\ttls.TLS_RSA_WITH_RC4_128_SHA, // BAD: TLS_RSA_WITH_RC4_128_SHA is one of the non-secure cipher suites; it's not safe to be used.\n\t\t},\n\t}\n\t_ = config\n}\n\n```\nThe following example shows how to create a safer TLS configuration:\n\n\n```go\npackage main\n\nimport \"crypto/tls\"\n\nfunc saferTLSConfig() {\n\tconfig := &tls.Config{}\n\tconfig.MinVersion = tls.VersionTLS12\n\tconfig.MaxVersion = tls.VersionTLS13\n\t// OR\n\tconfig.MaxVersion = 0 // GOOD: Setting MaxVersion to 0 means that the highest version available in the package will be used.\n}\n\n```\n\n## References\n* Wikipedia: [Transport Layer Security](https://en.wikipedia.org/wiki/Transport_Layer_Security)\n* Mozilla: [Security/Server Side TLS](https://wiki.mozilla.org/Security/Server_Side_TLS)\n* OWASP: [Transport Layer Protection Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Transport_Layer_Protection_Cheat_Sheet.html)\n* Common Weakness Enumeration: [CWE-327](https://cwe.mitre.org/data/definitions/327.html).\n" + }, + "properties" : { + "tags" : [ "security", "external/cwe/cwe-327" ], + "description" : "If an application supports insecure TLS versions or ciphers, it may be vulnerable to\n machine-in-the-middle and other attacks.", + "id" : "go/insecure-tls", + "kind" : "path-problem", + "name" : "Insecure TLS configuration", + "precision" : "very-high", + "problem.severity" : "warning", + "security-severity" : "7.5" + } + }, { + "id" : "go/summary/lines-of-code", + "name" : "go/summary/lines-of-code", + "shortDescription" : { + "text" : "Total lines of Go code in the database" + }, + "fullDescription" : { + "text" : "The total number of lines of Go code across all extracted files, including auto-generated files. This is a useful metric of the size of a database. For all files that were seen during the build, this query counts the lines of code, excluding whitespace or comments." + }, + "defaultConfiguration" : { + "enabled" : true + }, + "properties" : { + "tags" : [ "summary", "lines-of-code" ], + "description" : "The total number of lines of Go code across all extracted files, including auto-generated files. This is a useful metric of the size of a database. For all files that were seen during the build, this query counts the lines of code, excluding whitespace or comments.", + "id" : "go/summary/lines-of-code", + "kind" : "metric", + "name" : "Total lines of Go code in the database" + } + } ], + "locations" : [ { + "uri" : "file:///opt/hostedtoolcache/CodeQL/2.15.1/x64/codeql/qlpacks/codeql/go-queries/0.7.1/", + "description" : { + "text" : "The QL pack root directory." + } + }, { + "uri" : "file:///opt/hostedtoolcache/CodeQL/2.15.1/x64/codeql/qlpacks/codeql/go-queries/0.7.1/qlpack.yml", + "description" : { + "text" : "The QL pack definition file." + } + } ] + } ] + }, + "invocations" : [ { + "toolExecutionNotifications" : [ { + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "auth/go.mod", + "uriBaseId" : "%SRCROOT%", + "index" : 1 + } + } + } ], + "message" : { + "text" : "" + }, + "level" : "none", + "descriptor" : { + "id" : "go/diagnostics/successfully-extracted-files", + "index" : 0, + "toolComponent" : { + "index" : 0 + } + }, + "properties" : { + "formattedMessage" : { + "text" : "" + } + } + }, { + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/go.mod", + "uriBaseId" : "%SRCROOT%", + "index" : 2 + } + } + } ], + "message" : { + "text" : "" + }, + "level" : "none", + "descriptor" : { + "id" : "go/diagnostics/successfully-extracted-files", + "index" : 0, + "toolComponent" : { + "index" : 0 + } + }, + "properties" : { + "formattedMessage" : { + "text" : "" + } + } + }, { + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + } + } + } ], + "message" : { + "text" : "" + }, + "level" : "none", + "descriptor" : { + "id" : "go/diagnostics/successfully-extracted-files", + "index" : 0, + "toolComponent" : { + "index" : 0 + } + }, + "properties" : { + "formattedMessage" : { + "text" : "" + } + } + }, { + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "auth/op/login.go", + "uriBaseId" : "%SRCROOT%", + "index" : 3 + } + } + } ], + "message" : { + "text" : "" + }, + "level" : "none", + "descriptor" : { + "id" : "go/diagnostics/successfully-extracted-files", + "index" : 0, + "toolComponent" : { + "index" : 0 + } + }, + "properties" : { + "formattedMessage" : { + "text" : "" + } + } + }, { + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "auth/op/templates.go", + "uriBaseId" : "%SRCROOT%", + "index" : 4 + } + } + } ], + "message" : { + "text" : "" + }, + "level" : "none", + "descriptor" : { + "id" : "go/diagnostics/successfully-extracted-files", + "index" : 0, + "toolComponent" : { + "index" : 0 + } + }, + "properties" : { + "formattedMessage" : { + "text" : "" + } + } + }, { + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "auth/storage/oidc.go", + "uriBaseId" : "%SRCROOT%", + "index" : 5 + } + } + } ], + "message" : { + "text" : "" + }, + "level" : "none", + "descriptor" : { + "id" : "go/diagnostics/successfully-extracted-files", + "index" : 0, + "toolComponent" : { + "index" : 0 + } + }, + "properties" : { + "formattedMessage" : { + "text" : "" + } + } + }, { + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "auth/storage/storage.go", + "uriBaseId" : "%SRCROOT%", + "index" : 6 + } + } + } ], + "message" : { + "text" : "" + }, + "level" : "none", + "descriptor" : { + "id" : "go/diagnostics/successfully-extracted-files", + "index" : 0, + "toolComponent" : { + "index" : 0 + } + }, + "properties" : { + "formattedMessage" : { + "text" : "" + } + } + }, { + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "auth/storage/token.go", + "uriBaseId" : "%SRCROOT%", + "index" : 7 + } + } + } ], + "message" : { + "text" : "" + }, + "level" : "none", + "descriptor" : { + "id" : "go/diagnostics/successfully-extracted-files", + "index" : 0, + "toolComponent" : { + "index" : 0 + } + }, + "properties" : { + "formattedMessage" : { + "text" : "" + } + } + }, { + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "auth/storage/user.go", + "uriBaseId" : "%SRCROOT%", + "index" : 8 + } + } + } ], + "message" : { + "text" : "" + }, + "level" : "none", + "descriptor" : { + "id" : "go/diagnostics/successfully-extracted-files", + "index" : 0, + "toolComponent" : { + "index" : 0 + } + }, + "properties" : { + "formattedMessage" : { + "text" : "" + } + } + }, { + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "auth/op/templates/confirm_device.html", + "uriBaseId" : "%SRCROOT%", + "index" : 9 + } + } + } ], + "message" : { + "text" : "" + }, + "level" : "none", + "descriptor" : { + "id" : "go/diagnostics/successfully-extracted-files", + "index" : 0, + "toolComponent" : { + "index" : 0 + } + }, + "properties" : { + "formattedMessage" : { + "text" : "" + } + } + }, { + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "auth/op/templates/device_login.html", + "uriBaseId" : "%SRCROOT%", + "index" : 10 + } + } + } ], + "message" : { + "text" : "" + }, + "level" : "none", + "descriptor" : { + "id" : "go/diagnostics/successfully-extracted-files", + "index" : 0, + "toolComponent" : { + "index" : 0 + } + }, + "properties" : { + "formattedMessage" : { + "text" : "" + } + } + }, { + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "auth/op/templates/login.html", + "uriBaseId" : "%SRCROOT%", + "index" : 11 + } + } + } ], + "message" : { + "text" : "" + }, + "level" : "none", + "descriptor" : { + "id" : "go/diagnostics/successfully-extracted-files", + "index" : 0, + "toolComponent" : { + "index" : 0 + } + }, + "properties" : { + "formattedMessage" : { + "text" : "" + } + } + }, { + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "auth/op/templates/usercode.html", + "uriBaseId" : "%SRCROOT%", + "index" : 12 + } + } + } ], + "message" : { + "text" : "" + }, + "level" : "none", + "descriptor" : { + "id" : "go/diagnostics/successfully-extracted-files", + "index" : 0, + "toolComponent" : { + "index" : 0 + } + }, + "properties" : { + "formattedMessage" : { + "text" : "" + } + } + }, { + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "frontend/index.html", + "uriBaseId" : "%SRCROOT%", + "index" : 13 + } + } + } ], + "message" : { + "text" : "" + }, + "level" : "none", + "descriptor" : { + "id" : "go/diagnostics/successfully-extracted-files", + "index" : 0, + "toolComponent" : { + "index" : 0 + } + }, + "properties" : { + "formattedMessage" : { + "text" : "" + } + } + }, { + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "frontend/src/App.vue", + "uriBaseId" : "%SRCROOT%", + "index" : 14 + } + } + } ], + "message" : { + "text" : "" + }, + "level" : "none", + "descriptor" : { + "id" : "go/diagnostics/successfully-extracted-files", + "index" : 0, + "toolComponent" : { + "index" : 0 + } + }, + "properties" : { + "formattedMessage" : { + "text" : "" + } + } + }, { + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "frontend/src/components/AuthorizationCallback.vue", + "uriBaseId" : "%SRCROOT%", + "index" : 15 + } + } + } ], + "message" : { + "text" : "" + }, + "level" : "none", + "descriptor" : { + "id" : "go/diagnostics/successfully-extracted-files", + "index" : 0, + "toolComponent" : { + "index" : 0 + } + }, + "properties" : { + "formattedMessage" : { + "text" : "" + } + } + }, { + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "frontend/src/components/Gallery.vue", + "uriBaseId" : "%SRCROOT%", + "index" : 16 + } + } + } ], + "message" : { + "text" : "" + }, + "level" : "none", + "descriptor" : { + "id" : "go/diagnostics/successfully-extracted-files", + "index" : 0, + "toolComponent" : { + "index" : 0 + } + }, + "properties" : { + "formattedMessage" : { + "text" : "" + } + } + }, { + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "frontend/src/components/Login.vue", + "uriBaseId" : "%SRCROOT%", + "index" : 17 + } + } + } ], + "message" : { + "text" : "" + }, + "level" : "none", + "descriptor" : { + "id" : "go/diagnostics/successfully-extracted-files", + "index" : 0, + "toolComponent" : { + "index" : 0 + } + }, + "properties" : { + "formattedMessage" : { + "text" : "" + } + } + }, { + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "frontend/src/components/Logout.vue", + "uriBaseId" : "%SRCROOT%", + "index" : 18 + } + } + } ], + "message" : { + "text" : "" + }, + "level" : "none", + "descriptor" : { + "id" : "go/diagnostics/successfully-extracted-files", + "index" : 0, + "toolComponent" : { + "index" : 0 + } + }, + "properties" : { + "formattedMessage" : { + "text" : "" + } + } + }, { + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "frontend/src/components/NotFound.vue", + "uriBaseId" : "%SRCROOT%", + "index" : 19 + } + } + } ], + "message" : { + "text" : "" + }, + "level" : "none", + "descriptor" : { + "id" : "go/diagnostics/successfully-extracted-files", + "index" : 0, + "toolComponent" : { + "index" : 0 + } + }, + "properties" : { + "formattedMessage" : { + "text" : "" + } + } + }, { + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "frontend/src/views/AuthorizationCallbackView.vue", + "uriBaseId" : "%SRCROOT%", + "index" : 20 + } + } + } ], + "message" : { + "text" : "" + }, + "level" : "none", + "descriptor" : { + "id" : "go/diagnostics/successfully-extracted-files", + "index" : 0, + "toolComponent" : { + "index" : 0 + } + }, + "properties" : { + "formattedMessage" : { + "text" : "" + } + } + }, { + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "frontend/src/views/GalleryView.vue", + "uriBaseId" : "%SRCROOT%", + "index" : 21 + } + } + } ], + "message" : { + "text" : "" + }, + "level" : "none", + "descriptor" : { + "id" : "go/diagnostics/successfully-extracted-files", + "index" : 0, + "toolComponent" : { + "index" : 0 + } + }, + "properties" : { + "formattedMessage" : { + "text" : "" + } + } + }, { + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "frontend/src/views/LoginView.vue", + "uriBaseId" : "%SRCROOT%", + "index" : 22 + } + } + } ], + "message" : { + "text" : "" + }, + "level" : "none", + "descriptor" : { + "id" : "go/diagnostics/successfully-extracted-files", + "index" : 0, + "toolComponent" : { + "index" : 0 + } + }, + "properties" : { + "formattedMessage" : { + "text" : "" + } + } + }, { + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "frontend/src/views/NotFoundView.vue", + "uriBaseId" : "%SRCROOT%", + "index" : 23 + } + } + } ], + "message" : { + "text" : "" + }, + "level" : "none", + "descriptor" : { + "id" : "go/diagnostics/successfully-extracted-files", + "index" : 0, + "toolComponent" : { + "index" : 0 + } + }, + "properties" : { + "formattedMessage" : { + "text" : "" + } + } + }, { + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "frontend/src/views/LogoutView.vue", + "uriBaseId" : "%SRCROOT%", + "index" : 24 + } + } + } ], + "message" : { + "text" : "" + }, + "level" : "none", + "descriptor" : { + "id" : "go/diagnostics/successfully-extracted-files", + "index" : 0, + "toolComponent" : { + "index" : 0 + } + }, + "properties" : { + "formattedMessage" : { + "text" : "" + } + } + }, { + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "root/src/github.com/zitadel/oidc/example/server/exampleop/templates/confirm_device.html", + "uriBaseId" : "%SRCROOT%", + "index" : 25 + } + } + } ], + "message" : { + "text" : "" + }, + "level" : "none", + "descriptor" : { + "id" : "go/diagnostics/successfully-extracted-files", + "index" : 0, + "toolComponent" : { + "index" : 0 + } + }, + "properties" : { + "formattedMessage" : { + "text" : "" + } + } + }, { + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "root/src/github.com/zitadel/oidc/example/server/exampleop/templates/device_login.html", + "uriBaseId" : "%SRCROOT%", + "index" : 26 + } + } + } ], + "message" : { + "text" : "" + }, + "level" : "none", + "descriptor" : { + "id" : "go/diagnostics/successfully-extracted-files", + "index" : 0, + "toolComponent" : { + "index" : 0 + } + }, + "properties" : { + "formattedMessage" : { + "text" : "" + } + } + }, { + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "root/src/github.com/zitadel/oidc/example/server/exampleop/templates/login.html", + "uriBaseId" : "%SRCROOT%", + "index" : 27 + } + } + } ], + "message" : { + "text" : "" + }, + "level" : "none", + "descriptor" : { + "id" : "go/diagnostics/successfully-extracted-files", + "index" : 0, + "toolComponent" : { + "index" : 0 + } + }, + "properties" : { + "formattedMessage" : { + "text" : "" + } + } + }, { + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "root/src/github.com/zitadel/oidc/example/server/exampleop/templates/usercode.html", + "uriBaseId" : "%SRCROOT%", + "index" : 28 + } + } + } ], + "message" : { + "text" : "" + }, + "level" : "none", + "descriptor" : { + "id" : "go/diagnostics/successfully-extracted-files", + "index" : 0, + "toolComponent" : { + "index" : 0 + } + }, + "properties" : { + "formattedMessage" : { + "text" : "" + } + } + }, { + "message" : { + "text" : "Extraction failed in auth/main.go with error could not import auth/op (invalid package name: \"\")" + }, + "level" : "error", + "descriptor" : { + "id" : "go/diagnostics/extraction-errors", + "index" : 1, + "toolComponent" : { + "index" : 0 + } + }, + "properties" : { + "formattedMessage" : { + "text" : "Extraction failed in auth/main.go with error could not import auth/op (invalid package name: \"\")" + } + } + }, { + "message" : { + "text" : "Extraction failed in auth/main.go with error could not import auth/storage (invalid package name: \"\")" + }, + "level" : "error", + "descriptor" : { + "id" : "go/diagnostics/extraction-errors", + "index" : 1, + "toolComponent" : { + "index" : 0 + } + }, + "properties" : { + "formattedMessage" : { + "text" : "Extraction failed in auth/main.go with error could not import auth/storage (invalid package name: \"\")" + } + } + }, { + "message" : { + "text" : "Extraction failed in auth/op/op.go with error could not import auth/storage (invalid package name: \"\")" + }, + "level" : "error", + "descriptor" : { + "id" : "go/diagnostics/extraction-errors", + "index" : 1, + "toolComponent" : { + "index" : 0 + } + }, + "properties" : { + "formattedMessage" : { + "text" : "Extraction failed in auth/op/op.go with error could not import auth/storage (invalid package name: \"\")" + } + } + }, { + "message" : { + "text" : "Extraction failed in auth/op/device.go with error could not import github.com/zitadel/oidc/v2/pkg/op (invalid package name: \"\")" + }, + "level" : "error", + "descriptor" : { + "id" : "go/diagnostics/extraction-errors", + "index" : 1, + "toolComponent" : { + "index" : 0 + } + }, + "properties" : { + "formattedMessage" : { + "text" : "Extraction failed in auth/op/device.go with error could not import github.com/zitadel/oidc/v2/pkg/op (invalid package name: \"\")" + } + } + }, { + "message" : { + "text" : "Extraction failed in auth/storage/client.go with error could not import github.com/zitadel/oidc/v2/pkg/op (invalid package name: \"\")" + }, + "level" : "error", + "descriptor" : { + "id" : "go/diagnostics/extraction-errors", + "index" : 1, + "toolComponent" : { + "index" : 0 + } + }, + "properties" : { + "formattedMessage" : { + "text" : "Extraction failed in auth/storage/client.go with error could not import github.com/zitadel/oidc/v2/pkg/op (invalid package name: \"\")" + } + } + }, { + "message" : { + "text" : "Extraction failed in auth/op/device.go with error d.storage.GetDeviceAuthorizationByUserCode undefined (type deviceAuthenticate has no field or method GetDeviceAuthorizationByUserCode)" + }, + "level" : "error", + "descriptor" : { + "id" : "go/diagnostics/extraction-errors", + "index" : 1, + "toolComponent" : { + "index" : 0 + } + }, + "properties" : { + "formattedMessage" : { + "text" : "Extraction failed in auth/op/device.go with error d.storage.GetDeviceAuthorizationByUserCode undefined (type deviceAuthenticate has no field or method GetDeviceAuthorizationByUserCode)" + } + } + }, { + "message" : { + "text" : "Extraction failed in auth/op/device.go with error d.storage.CompleteDeviceAuthorization undefined (type deviceAuthenticate has no field or method CompleteDeviceAuthorization)" + }, + "level" : "error", + "descriptor" : { + "id" : "go/diagnostics/extraction-errors", + "index" : 1, + "toolComponent" : { + "index" : 0 + } + }, + "properties" : { + "formattedMessage" : { + "text" : "Extraction failed in auth/op/device.go with error d.storage.CompleteDeviceAuthorization undefined (type deviceAuthenticate has no field or method CompleteDeviceAuthorization)" + } + } + }, { + "message" : { + "text" : "Extraction failed in auth/op/device.go with error d.storage.DenyDeviceAuthorization undefined (type deviceAuthenticate has no field or method DenyDeviceAuthorization)" + }, + "level" : "error", + "descriptor" : { + "id" : "go/diagnostics/extraction-errors", + "index" : 1, + "toolComponent" : { + "index" : 0 + } + }, + "properties" : { + "formattedMessage" : { + "text" : "Extraction failed in auth/op/device.go with error d.storage.DenyDeviceAuthorization undefined (type deviceAuthenticate has no field or method DenyDeviceAuthorization)" + } + } + }, { + "message" : { + "text" : "Extraction failed in auth/storage/client.go with error could not import github.com/zitadel/oidc/v2/pkg/oidc (invalid package name: \"\")" + }, + "level" : "error", + "descriptor" : { + "id" : "go/diagnostics/extraction-errors", + "index" : 1, + "toolComponent" : { + "index" : 0 + } + }, + "properties" : { + "formattedMessage" : { + "text" : "Extraction failed in auth/storage/client.go with error could not import github.com/zitadel/oidc/v2/pkg/oidc (invalid package name: \"\")" + } + } + }, { + "message" : { + "text" : "Extraction failed in auth/main.go with error cannot find package \"auth/storage\" in any of:\n\t(absolute path) (from $GOROOT)\n\t(absolute path) (from $GOPATH)" + }, + "level" : "error", + "descriptor" : { + "id" : "go/diagnostics/extraction-errors", + "index" : 1, + "toolComponent" : { + "index" : 0 + } + }, + "properties" : { + "formattedMessage" : { + "text" : "Extraction failed in auth/main.go with error cannot find package \"auth/storage\" in any of:\n\t(absolute path) (from $GOROOT)\n\t(absolute path) (from $GOPATH)" + } + } + }, { + "message" : { + "text" : "Extraction failed in auth/main.go with error cannot find package \"auth/op\" in any of:\n\t(absolute path) (from $GOROOT)\n\t(absolute path) (from $GOPATH)" + }, + "level" : "error", + "descriptor" : { + "id" : "go/diagnostics/extraction-errors", + "index" : 1, + "toolComponent" : { + "index" : 0 + } + }, + "properties" : { + "formattedMessage" : { + "text" : "Extraction failed in auth/main.go with error cannot find package \"auth/op\" in any of:\n\t(absolute path) (from $GOROOT)\n\t(absolute path) (from $GOPATH)" + } + } + }, { + "message" : { + "text" : "Extraction failed in auth/op/device.go with error cannot find package \"github.com/zitadel/oidc/v2/pkg/op\" in any of:\n\t(absolute path) (from $GOROOT)\n\t(absolute path) (from $GOPATH)" + }, + "level" : "error", + "descriptor" : { + "id" : "go/diagnostics/extraction-errors", + "index" : 1, + "toolComponent" : { + "index" : 0 + } + }, + "properties" : { + "formattedMessage" : { + "text" : "Extraction failed in auth/op/device.go with error cannot find package \"github.com/zitadel/oidc/v2/pkg/op\" in any of:\n\t(absolute path) (from $GOROOT)\n\t(absolute path) (from $GOPATH)" + } + } + }, { + "message" : { + "text" : "Extraction failed in auth/storage/client.go with error cannot find package \"github.com/zitadel/oidc/v2/pkg/oidc\" in any of:\n\t(absolute path) (from $GOROOT)\n\t(absolute path) (from $GOPATH)" + }, + "level" : "error", + "descriptor" : { + "id" : "go/diagnostics/extraction-errors", + "index" : 1, + "toolComponent" : { + "index" : 0 + } + }, + "properties" : { + "formattedMessage" : { + "text" : "Extraction failed in auth/storage/client.go with error cannot find package \"github.com/zitadel/oidc/v2/pkg/oidc\" in any of:\n\t(absolute path) (from $GOROOT)\n\t(absolute path) (from $GOPATH)" + } + } + }, { + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "storage/src/main/java/com/github/advancedsecurity/storageservice/security/JwtAuthenticationException.java", + "uriBaseId" : "%SRCROOT%", + "index" : 29 + } + } + } ], + "message" : { + "text" : "" + }, + "level" : "none", + "descriptor" : { + "id" : "cli/expected-extracted-files/java", + "index" : 0 + }, + "properties" : { + "formattedMessage" : { + "text" : "" + } + } + }, { + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "storage/src/main/java/com/github/advancedsecurity/storageservice/config/MinioConfig.java", + "uriBaseId" : "%SRCROOT%", + "index" : 30 + } + } + } ], + "message" : { + "text" : "" + }, + "level" : "none", + "descriptor" : { + "id" : "cli/expected-extracted-files/java", + "index" : 0 + }, + "properties" : { + "formattedMessage" : { + "text" : "" + } + } + }, { + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "storage/src/main/java/com/github/advancedsecurity/storageservice/security/JwtAuthenticationToken.java", + "uriBaseId" : "%SRCROOT%", + "index" : 31 + } + } + } ], + "message" : { + "text" : "" + }, + "level" : "none", + "descriptor" : { + "id" : "cli/expected-extracted-files/java", + "index" : 0 + }, + "properties" : { + "formattedMessage" : { + "text" : "" + } + } + }, { + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "storage/src/main/java/com/github/advancedsecurity/storageservice/models/Blob.java", + "uriBaseId" : "%SRCROOT%", + "index" : 32 + } + } + } ], + "message" : { + "text" : "" + }, + "level" : "none", + "descriptor" : { + "id" : "cli/expected-extracted-files/java", + "index" : 0 + }, + "properties" : { + "formattedMessage" : { + "text" : "" + } + } + }, { + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "storage/src/main/java/com/github/advancedsecurity/storageservice/security/JwtAuthenticationProvider.java", + "uriBaseId" : "%SRCROOT%", + "index" : 33 + } + } + } ], + "message" : { + "text" : "" + }, + "level" : "none", + "descriptor" : { + "id" : "cli/expected-extracted-files/java", + "index" : 0 + }, + "properties" : { + "formattedMessage" : { + "text" : "" + } + } + }, { + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "storage/src/main/java/com/github/advancedsecurity/storageservice/models/Profile.java", + "uriBaseId" : "%SRCROOT%", + "index" : 34 + } + } + } ], + "message" : { + "text" : "" + }, + "level" : "none", + "descriptor" : { + "id" : "cli/expected-extracted-files/java", + "index" : 0 + }, + "properties" : { + "formattedMessage" : { + "text" : "" + } + } + }, { + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "storage/src/main/java/com/github/advancedsecurity/storageservice/controllers/BlobController.java", + "uriBaseId" : "%SRCROOT%", + "index" : 35 + } + } + } ], + "message" : { + "text" : "" + }, + "level" : "none", + "descriptor" : { + "id" : "cli/expected-extracted-files/java", + "index" : 0 + }, + "properties" : { + "formattedMessage" : { + "text" : "" + } + } + }, { + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "storage/src/main/java/com/github/advancedsecurity/storageservice/security/BearerAuthenticationFilter.java", + "uriBaseId" : "%SRCROOT%", + "index" : 36 + } + } + } ], + "message" : { + "text" : "" + }, + "level" : "none", + "descriptor" : { + "id" : "cli/expected-extracted-files/java", + "index" : 0 + }, + "properties" : { + "formattedMessage" : { + "text" : "" + } + } + }, { + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "storage/src/main/java/com/github/advancedsecurity/storageservice/App.java", + "uriBaseId" : "%SRCROOT%", + "index" : 37 + } + } + } ], + "message" : { + "text" : "" + }, + "level" : "none", + "descriptor" : { + "id" : "cli/expected-extracted-files/java", + "index" : 0 + }, + "properties" : { + "formattedMessage" : { + "text" : "" + } + } + }, { + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "storage/src/main/java/com/github/advancedsecurity/storageservice/security/JwtAuthenticationSuccessHandler.java", + "uriBaseId" : "%SRCROOT%", + "index" : 38 + } + } + } ], + "message" : { + "text" : "" + }, + "level" : "none", + "descriptor" : { + "id" : "cli/expected-extracted-files/java", + "index" : 0 + }, + "properties" : { + "formattedMessage" : { + "text" : "" + } + } + }, { + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "storage/src/main/java/com/github/advancedsecurity/storageservice/security/JwtAccessDeniedHandler.java", + "uriBaseId" : "%SRCROOT%", + "index" : 39 + } + } + } ], + "message" : { + "text" : "" + }, + "level" : "none", + "descriptor" : { + "id" : "cli/expected-extracted-files/java", + "index" : 0 + }, + "properties" : { + "formattedMessage" : { + "text" : "" + } + } + }, { + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "storage/src/main/java/com/github/advancedsecurity/storageservice/WebSecurityConfig.java", + "uriBaseId" : "%SRCROOT%", + "index" : 40 + } + } + } ], + "message" : { + "text" : "" + }, + "level" : "none", + "descriptor" : { + "id" : "cli/expected-extracted-files/java", + "index" : 0 + }, + "properties" : { + "formattedMessage" : { + "text" : "" + } + } + }, { + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "storage/src/main/java/com/github/advancedsecurity/storageservice/security/JwtAuthenticationEntryPoint.java", + "uriBaseId" : "%SRCROOT%", + "index" : 41 + } + } + } ], + "message" : { + "text" : "" + }, + "level" : "none", + "descriptor" : { + "id" : "cli/expected-extracted-files/java", + "index" : 0 + }, + "properties" : { + "formattedMessage" : { + "text" : "" + } + } + }, { + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "auth/op/op.go", + "uriBaseId" : "%SRCROOT%", + "index" : 42 + } + } + } ], + "message" : { + "text" : "" + }, + "level" : "none", + "descriptor" : { + "id" : "cli/expected-extracted-files/go", + "index" : 1 + }, + "properties" : { + "formattedMessage" : { + "text" : "" + } + } + }, { + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "auth/storage/storage.go", + "uriBaseId" : "%SRCROOT%", + "index" : 6 + } + } + } ], + "message" : { + "text" : "" + }, + "level" : "none", + "descriptor" : { + "id" : "cli/expected-extracted-files/go", + "index" : 1 + }, + "properties" : { + "formattedMessage" : { + "text" : "" + } + } + }, { + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "auth/storage/token.go", + "uriBaseId" : "%SRCROOT%", + "index" : 7 + } + } + } ], + "message" : { + "text" : "" + }, + "level" : "none", + "descriptor" : { + "id" : "cli/expected-extracted-files/go", + "index" : 1 + }, + "properties" : { + "formattedMessage" : { + "text" : "" + } + } + }, { + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "auth/op/device.go", + "uriBaseId" : "%SRCROOT%", + "index" : 43 + } + } + } ], + "message" : { + "text" : "" + }, + "level" : "none", + "descriptor" : { + "id" : "cli/expected-extracted-files/go", + "index" : 1 + }, + "properties" : { + "formattedMessage" : { + "text" : "" + } + } + }, { + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "auth/storage/user.go", + "uriBaseId" : "%SRCROOT%", + "index" : 8 + } + } + } ], + "message" : { + "text" : "" + }, + "level" : "none", + "descriptor" : { + "id" : "cli/expected-extracted-files/go", + "index" : 1 + }, + "properties" : { + "formattedMessage" : { + "text" : "" + } + } + }, { + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "auth/storage/oidc.go", + "uriBaseId" : "%SRCROOT%", + "index" : 5 + } + } + } ], + "message" : { + "text" : "" + }, + "level" : "none", + "descriptor" : { + "id" : "cli/expected-extracted-files/go", + "index" : 1 + }, + "properties" : { + "formattedMessage" : { + "text" : "" + } + } + }, { + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "auth/op/templates.go", + "uriBaseId" : "%SRCROOT%", + "index" : 4 + } + } + } ], + "message" : { + "text" : "" + }, + "level" : "none", + "descriptor" : { + "id" : "cli/expected-extracted-files/go", + "index" : 1 + }, + "properties" : { + "formattedMessage" : { + "text" : "" + } + } + }, { + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "auth/op/login.go", + "uriBaseId" : "%SRCROOT%", + "index" : 3 + } + } + } ], + "message" : { + "text" : "" + }, + "level" : "none", + "descriptor" : { + "id" : "cli/expected-extracted-files/go", + "index" : 1 + }, + "properties" : { + "formattedMessage" : { + "text" : "" + } + } + }, { + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "auth/storage/client.go", + "uriBaseId" : "%SRCROOT%", + "index" : 44 + } + } + } ], + "message" : { + "text" : "" + }, + "level" : "none", + "descriptor" : { + "id" : "cli/expected-extracted-files/go", + "index" : 1 + }, + "properties" : { + "formattedMessage" : { + "text" : "" + } + } + }, { + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "auth/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 45 + } + } + } ], + "message" : { + "text" : "" + }, + "level" : "none", + "descriptor" : { + "id" : "cli/expected-extracted-files/go", + "index" : 1 + }, + "properties" : { + "formattedMessage" : { + "text" : "" + } + } + }, { + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + } + } + } ], + "message" : { + "text" : "" + }, + "level" : "none", + "descriptor" : { + "id" : "cli/expected-extracted-files/go", + "index" : 1 + }, + "properties" : { + "formattedMessage" : { + "text" : "" + } + } + }, { + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "frontend/src/stores/counter.js", + "uriBaseId" : "%SRCROOT%", + "index" : 46 + } + } + } ], + "message" : { + "text" : "" + }, + "level" : "none", + "descriptor" : { + "id" : "cli/expected-extracted-files/javascript", + "index" : 2 + }, + "properties" : { + "formattedMessage" : { + "text" : "" + } + } + }, { + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "frontend/src/main.js", + "uriBaseId" : "%SRCROOT%", + "index" : 47 + } + } + } ], + "message" : { + "text" : "" + }, + "level" : "none", + "descriptor" : { + "id" : "cli/expected-extracted-files/javascript", + "index" : 2 + }, + "properties" : { + "formattedMessage" : { + "text" : "" + } + } + }, { + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "frontend/vite.config.js", + "uriBaseId" : "%SRCROOT%", + "index" : 48 + } + } + } ], + "message" : { + "text" : "" + }, + "level" : "none", + "descriptor" : { + "id" : "cli/expected-extracted-files/javascript", + "index" : 2 + }, + "properties" : { + "formattedMessage" : { + "text" : "" + } + } + }, { + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "frontend/src/stores/store.js", + "uriBaseId" : "%SRCROOT%", + "index" : 49 + } + } + } ], + "message" : { + "text" : "" + }, + "level" : "none", + "descriptor" : { + "id" : "cli/expected-extracted-files/javascript", + "index" : 2 + }, + "properties" : { + "formattedMessage" : { + "text" : "" + } + } + }, { + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "frontend/src/router/index.js", + "uriBaseId" : "%SRCROOT%", + "index" : 50 + } + } + } ], + "message" : { + "text" : "" + }, + "level" : "none", + "descriptor" : { + "id" : "cli/expected-extracted-files/javascript", + "index" : 2 + }, + "properties" : { + "formattedMessage" : { + "text" : "" + } + } + }, { + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "auth-ext/app.py", + "uriBaseId" : "%SRCROOT%", + "index" : 51 + } + } + } ], + "message" : { + "text" : "" + }, + "level" : "none", + "descriptor" : { + "id" : "cli/expected-extracted-files/python", + "index" : 3 + }, + "properties" : { + "formattedMessage" : { + "text" : "" + } + } + }, { + "message" : { + "text" : "2 `go.mod` files were found:\n\n`auth/go.mod`, `gallery/go.mod`", + "markdown" : "2 `go.mod` files were found:\n\n`auth/go.mod`, `gallery/go.mod`" + }, + "level" : "note", + "timeUtc" : "2023-10-26T05:08:51.645+00:00", + "descriptor" : { + "id" : "go/autobuilder/multiple-go-mod-found-not-nested", + "index" : 4 + }, + "properties" : { + "visibility" : { + "statusPage" : false, + "telemetry" : true + } + } + }, { + "message" : { + "text" : "4 packages could not be found:\n\n`auth/op`, `auth/storage`, `github.com/zitadel/oidc/v2/pkg/op`, `github.com/zitadel/oidc/v2/pkg/oidc`.\n\nDefinitions in those packages may not be recognized by CodeQL, and files that use them may only be partially analyzed.\n\nCheck that the paths are correct and make sure any private packages can be accessed. If any of the packages are present in the repository then you may need a [custom build command](https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-the-codeql-workflow-for-compiled-languages).", + "markdown" : "4 packages could not be found:\n\n`auth/op`, `auth/storage`, `github.com/zitadel/oidc/v2/pkg/op`, `github.com/zitadel/oidc/v2/pkg/oidc`.\n\nDefinitions in those packages may not be recognized by CodeQL, and files that use them may only be partially analyzed.\n\nCheck that the paths are correct and make sure any private packages can be accessed. If any of the packages are present in the repository then you may need a [custom build command](https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-the-codeql-workflow-for-compiled-languages)." + }, + "timeUtc" : "2023-10-26T05:10:13.766+00:00", + "descriptor" : { + "id" : "go/autobuilder/package-not-found", + "index" : 5 + }, + "properties" : { + "visibility" : { + "statusPage" : true, + "telemetry" : true + } + } + } ], + "executionSuccessful" : true + } ], + "artifacts" : [ { + "location" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + } + }, { + "location" : { + "uri" : "auth/go.mod", + "uriBaseId" : "%SRCROOT%", + "index" : 1 + } + }, { + "location" : { + "uri" : "gallery/go.mod", + "uriBaseId" : "%SRCROOT%", + "index" : 2 + } + }, { + "location" : { + "uri" : "auth/op/login.go", + "uriBaseId" : "%SRCROOT%", + "index" : 3 + } + }, { + "location" : { + "uri" : "auth/op/templates.go", + "uriBaseId" : "%SRCROOT%", + "index" : 4 + } + }, { + "location" : { + "uri" : "auth/storage/oidc.go", + "uriBaseId" : "%SRCROOT%", + "index" : 5 + } + }, { + "location" : { + "uri" : "auth/storage/storage.go", + "uriBaseId" : "%SRCROOT%", + "index" : 6 + } + }, { + "location" : { + "uri" : "auth/storage/token.go", + "uriBaseId" : "%SRCROOT%", + "index" : 7 + } + }, { + "location" : { + "uri" : "auth/storage/user.go", + "uriBaseId" : "%SRCROOT%", + "index" : 8 + } + }, { + "location" : { + "uri" : "auth/op/templates/confirm_device.html", + "uriBaseId" : "%SRCROOT%", + "index" : 9 + } + }, { + "location" : { + "uri" : "auth/op/templates/device_login.html", + "uriBaseId" : "%SRCROOT%", + "index" : 10 + } + }, { + "location" : { + "uri" : "auth/op/templates/login.html", + "uriBaseId" : "%SRCROOT%", + "index" : 11 + } + }, { + "location" : { + "uri" : "auth/op/templates/usercode.html", + "uriBaseId" : "%SRCROOT%", + "index" : 12 + } + }, { + "location" : { + "uri" : "frontend/index.html", + "uriBaseId" : "%SRCROOT%", + "index" : 13 + } + }, { + "location" : { + "uri" : "frontend/src/App.vue", + "uriBaseId" : "%SRCROOT%", + "index" : 14 + } + }, { + "location" : { + "uri" : "frontend/src/components/AuthorizationCallback.vue", + "uriBaseId" : "%SRCROOT%", + "index" : 15 + } + }, { + "location" : { + "uri" : "frontend/src/components/Gallery.vue", + "uriBaseId" : "%SRCROOT%", + "index" : 16 + } + }, { + "location" : { + "uri" : "frontend/src/components/Login.vue", + "uriBaseId" : "%SRCROOT%", + "index" : 17 + } + }, { + "location" : { + "uri" : "frontend/src/components/Logout.vue", + "uriBaseId" : "%SRCROOT%", + "index" : 18 + } + }, { + "location" : { + "uri" : "frontend/src/components/NotFound.vue", + "uriBaseId" : "%SRCROOT%", + "index" : 19 + } + }, { + "location" : { + "uri" : "frontend/src/views/AuthorizationCallbackView.vue", + "uriBaseId" : "%SRCROOT%", + "index" : 20 + } + }, { + "location" : { + "uri" : "frontend/src/views/GalleryView.vue", + "uriBaseId" : "%SRCROOT%", + "index" : 21 + } + }, { + "location" : { + "uri" : "frontend/src/views/LoginView.vue", + "uriBaseId" : "%SRCROOT%", + "index" : 22 + } + }, { + "location" : { + "uri" : "frontend/src/views/NotFoundView.vue", + "uriBaseId" : "%SRCROOT%", + "index" : 23 + } + }, { + "location" : { + "uri" : "frontend/src/views/LogoutView.vue", + "uriBaseId" : "%SRCROOT%", + "index" : 24 + } + }, { + "location" : { + "uri" : "root/src/github.com/zitadel/oidc/example/server/exampleop/templates/confirm_device.html", + "uriBaseId" : "%SRCROOT%", + "index" : 25 + } + }, { + "location" : { + "uri" : "root/src/github.com/zitadel/oidc/example/server/exampleop/templates/device_login.html", + "uriBaseId" : "%SRCROOT%", + "index" : 26 + } + }, { + "location" : { + "uri" : "root/src/github.com/zitadel/oidc/example/server/exampleop/templates/login.html", + "uriBaseId" : "%SRCROOT%", + "index" : 27 + } + }, { + "location" : { + "uri" : "root/src/github.com/zitadel/oidc/example/server/exampleop/templates/usercode.html", + "uriBaseId" : "%SRCROOT%", + "index" : 28 + } + }, { + "location" : { + "uri" : "storage/src/main/java/com/github/advancedsecurity/storageservice/security/JwtAuthenticationException.java", + "uriBaseId" : "%SRCROOT%", + "index" : 29 + } + }, { + "location" : { + "uri" : "storage/src/main/java/com/github/advancedsecurity/storageservice/config/MinioConfig.java", + "uriBaseId" : "%SRCROOT%", + "index" : 30 + } + }, { + "location" : { + "uri" : "storage/src/main/java/com/github/advancedsecurity/storageservice/security/JwtAuthenticationToken.java", + "uriBaseId" : "%SRCROOT%", + "index" : 31 + } + }, { + "location" : { + "uri" : "storage/src/main/java/com/github/advancedsecurity/storageservice/models/Blob.java", + "uriBaseId" : "%SRCROOT%", + "index" : 32 + } + }, { + "location" : { + "uri" : "storage/src/main/java/com/github/advancedsecurity/storageservice/security/JwtAuthenticationProvider.java", + "uriBaseId" : "%SRCROOT%", + "index" : 33 + } + }, { + "location" : { + "uri" : "storage/src/main/java/com/github/advancedsecurity/storageservice/models/Profile.java", + "uriBaseId" : "%SRCROOT%", + "index" : 34 + } + }, { + "location" : { + "uri" : "storage/src/main/java/com/github/advancedsecurity/storageservice/controllers/BlobController.java", + "uriBaseId" : "%SRCROOT%", + "index" : 35 + } + }, { + "location" : { + "uri" : "storage/src/main/java/com/github/advancedsecurity/storageservice/security/BearerAuthenticationFilter.java", + "uriBaseId" : "%SRCROOT%", + "index" : 36 + } + }, { + "location" : { + "uri" : "storage/src/main/java/com/github/advancedsecurity/storageservice/App.java", + "uriBaseId" : "%SRCROOT%", + "index" : 37 + } + }, { + "location" : { + "uri" : "storage/src/main/java/com/github/advancedsecurity/storageservice/security/JwtAuthenticationSuccessHandler.java", + "uriBaseId" : "%SRCROOT%", + "index" : 38 + } + }, { + "location" : { + "uri" : "storage/src/main/java/com/github/advancedsecurity/storageservice/security/JwtAccessDeniedHandler.java", + "uriBaseId" : "%SRCROOT%", + "index" : 39 + } + }, { + "location" : { + "uri" : "storage/src/main/java/com/github/advancedsecurity/storageservice/WebSecurityConfig.java", + "uriBaseId" : "%SRCROOT%", + "index" : 40 + } + }, { + "location" : { + "uri" : "storage/src/main/java/com/github/advancedsecurity/storageservice/security/JwtAuthenticationEntryPoint.java", + "uriBaseId" : "%SRCROOT%", + "index" : 41 + } + }, { + "location" : { + "uri" : "auth/op/op.go", + "uriBaseId" : "%SRCROOT%", + "index" : 42 + } + }, { + "location" : { + "uri" : "auth/op/device.go", + "uriBaseId" : "%SRCROOT%", + "index" : 43 + } + }, { + "location" : { + "uri" : "auth/storage/client.go", + "uriBaseId" : "%SRCROOT%", + "index" : 44 + } + }, { + "location" : { + "uri" : "auth/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 45 + } + }, { + "location" : { + "uri" : "frontend/src/stores/counter.js", + "uriBaseId" : "%SRCROOT%", + "index" : 46 + } + }, { + "location" : { + "uri" : "frontend/src/main.js", + "uriBaseId" : "%SRCROOT%", + "index" : 47 + } + }, { + "location" : { + "uri" : "frontend/vite.config.js", + "uriBaseId" : "%SRCROOT%", + "index" : 48 + } + }, { + "location" : { + "uri" : "frontend/src/stores/store.js", + "uriBaseId" : "%SRCROOT%", + "index" : 49 + } + }, { + "location" : { + "uri" : "frontend/src/router/index.js", + "uriBaseId" : "%SRCROOT%", + "index" : 50 + } + }, { + "location" : { + "uri" : "auth-ext/app.py", + "uriBaseId" : "%SRCROOT%", + "index" : 51 + } + } ], + "results" : [ { + "ruleId" : "go/sql-injection", + "rule" : { + "id" : "go/sql-injection", + "index" : 8, + "toolComponent" : { + "index" : 0 + } + }, + "message" : { + "text" : "This query depends on a [user-provided value](1).\nThis query depends on a [user-provided value](2).\nThis query depends on a [user-provided value](3)." + }, + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 200, + "startColumn" : 26, + "endColumn" : 31 + } + } + } ], + "partialFingerprints" : { + "primaryLocationLineHash" : "3a837e3cc6892848:1", + "primaryLocationStartColumnFingerprint" : "24" + }, + "codeFlows" : [ { + "threadFlows" : [ { + "locations" : [ { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 352, + "startColumn" : 17, + "endColumn" : 25 + } + }, + "message" : { + "text" : "selection of Header" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 352, + "startColumn" : 17, + "endColumn" : 56 + } + }, + "message" : { + "text" : "call to Get" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 352, + "startColumn" : 2, + "endColumn" : 9 + } + }, + "message" : { + "text" : "implicit dereference [Name]" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 350, + "startColumn" : 2, + "endColumn" : 9 + } + }, + "message" : { + "text" : "definition of profile [pointer, Name]" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 356, + "startColumn" : 9, + "endColumn" : 16 + } + }, + "message" : { + "text" : "profile [pointer, Name]" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 440, + "startColumn" : 13, + "endColumn" : 26 + } + }, + "message" : { + "text" : "call to GetProfile [pointer, Name]" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 451, + "startColumn" : 48, + "endColumn" : 55 + } + }, + "message" : { + "text" : "profile [pointer, Name]" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 451, + "startColumn" : 48, + "endColumn" : 55 + } + }, + "message" : { + "text" : "implicit dereference [Name]" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 451, + "startColumn" : 48, + "endColumn" : 60 + } + }, + "message" : { + "text" : "selection of Name" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 451, + "startColumn" : 19, + "endColumn" : 60 + } + }, + "message" : { + "text" : "...+..." + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 441, + "startColumn" : 6, + "endColumn" : 13 + } + }, + "message" : { + "text" : "definition of gallery [Title]" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 453, + "startColumn" : 10, + "endColumn" : 17 + } + }, + "message" : { + "text" : "gallery [Title]" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 196, + "startColumn" : 7, + "endColumn" : 8 + } + }, + "message" : { + "text" : "definition of g [Title]" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 198, + "startColumn" : 109, + "endColumn" : 110 + } + }, + "message" : { + "text" : "g [Title]" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 198, + "startColumn" : 109, + "endColumn" : 116 + } + }, + "message" : { + "text" : "selection of Title" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 198, + "startColumn" : 11, + "endColumn" : 153 + } + }, + "message" : { + "text" : "call to Sprintf" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 200, + "startColumn" : 26, + "endColumn" : 31 + } + }, + "message" : { + "text" : "query" + } + } + } ] + } ] + }, { + "threadFlows" : [ { + "locations" : [ { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 353, + "startColumn" : 18, + "endColumn" : 26 + } + }, + "message" : { + "text" : "selection of Header" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 353, + "startColumn" : 18, + "endColumn" : 58 + } + }, + "message" : { + "text" : "call to Get" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 353, + "startColumn" : 2, + "endColumn" : 9 + } + }, + "message" : { + "text" : "implicit dereference [Email]" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 350, + "startColumn" : 2, + "endColumn" : 9 + } + }, + "message" : { + "text" : "definition of profile [pointer, Email]" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 354, + "startColumn" : 18, + "endColumn" : 25 + } + }, + "message" : { + "text" : "profile [pointer, Email]" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 354, + "startColumn" : 18, + "endColumn" : 25 + } + }, + "message" : { + "text" : "implicit dereference [Email]" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 354, + "startColumn" : 18, + "endColumn" : 31 + } + }, + "message" : { + "text" : "selection of Email" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 354, + "startColumn" : 2, + "endColumn" : 9 + } + }, + "message" : { + "text" : "implicit dereference [Login]" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 350, + "startColumn" : 2, + "endColumn" : 9 + } + }, + "message" : { + "text" : "definition of profile [pointer, Login]" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 356, + "startColumn" : 9, + "endColumn" : 16 + } + }, + "message" : { + "text" : "profile [pointer, Login]" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 373, + "startColumn" : 13, + "endColumn" : 26 + } + }, + "message" : { + "text" : "call to GetProfile [pointer, Login]" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 383, + "startColumn" : 23, + "endColumn" : 30 + } + }, + "message" : { + "text" : "profile [pointer, Login]" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 196, + "startColumn" : 25, + "endColumn" : 32 + } + }, + "message" : { + "text" : "definition of profile [pointer, Login]" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 198, + "startColumn" : 139, + "endColumn" : 146 + } + }, + "message" : { + "text" : "profile [pointer, Login]" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 198, + "startColumn" : 139, + "endColumn" : 146 + } + }, + "message" : { + "text" : "implicit dereference [Login]" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 198, + "startColumn" : 139, + "endColumn" : 152 + } + }, + "message" : { + "text" : "selection of Login" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 198, + "startColumn" : 11, + "endColumn" : 153 + } + }, + "message" : { + "text" : "call to Sprintf" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 200, + "startColumn" : 26, + "endColumn" : 31 + } + }, + "message" : { + "text" : "query" + } + } + } ] + } ] + }, { + "threadFlows" : [ { + "locations" : [ { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 353, + "startColumn" : 18, + "endColumn" : 26 + } + }, + "message" : { + "text" : "selection of Header" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 353, + "startColumn" : 18, + "endColumn" : 58 + } + }, + "message" : { + "text" : "call to Get" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 353, + "startColumn" : 2, + "endColumn" : 9 + } + }, + "message" : { + "text" : "implicit dereference [Email]" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 350, + "startColumn" : 2, + "endColumn" : 9 + } + }, + "message" : { + "text" : "definition of profile [pointer, Email]" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 354, + "startColumn" : 18, + "endColumn" : 25 + } + }, + "message" : { + "text" : "profile [pointer, Email]" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 354, + "startColumn" : 18, + "endColumn" : 25 + } + }, + "message" : { + "text" : "implicit dereference [Email]" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 354, + "startColumn" : 18, + "endColumn" : 31 + } + }, + "message" : { + "text" : "selection of Email" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 354, + "startColumn" : 2, + "endColumn" : 9 + } + }, + "message" : { + "text" : "implicit dereference [Login]" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 350, + "startColumn" : 2, + "endColumn" : 9 + } + }, + "message" : { + "text" : "definition of profile [pointer, Login]" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 356, + "startColumn" : 9, + "endColumn" : 16 + } + }, + "message" : { + "text" : "profile [pointer, Login]" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 440, + "startColumn" : 13, + "endColumn" : 26 + } + }, + "message" : { + "text" : "call to GetProfile [pointer, Login]" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 453, + "startColumn" : 25, + "endColumn" : 32 + } + }, + "message" : { + "text" : "profile [pointer, Login]" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 196, + "startColumn" : 25, + "endColumn" : 32 + } + }, + "message" : { + "text" : "definition of profile [pointer, Login]" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 198, + "startColumn" : 139, + "endColumn" : 146 + } + }, + "message" : { + "text" : "profile [pointer, Login]" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 198, + "startColumn" : 139, + "endColumn" : 146 + } + }, + "message" : { + "text" : "implicit dereference [Login]" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 198, + "startColumn" : 139, + "endColumn" : 152 + } + }, + "message" : { + "text" : "selection of Login" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 198, + "startColumn" : 11, + "endColumn" : 153 + } + }, + "message" : { + "text" : "call to Sprintf" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 200, + "startColumn" : 26, + "endColumn" : 31 + } + }, + "message" : { + "text" : "query" + } + } + } ] + } ] + }, { + "threadFlows" : [ { + "locations" : [ { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 377, + "startColumn" : 25, + "endColumn" : 31 + } + }, + "message" : { + "text" : "selection of Body" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 377, + "startColumn" : 9, + "endColumn" : 32 + } + }, + "message" : { + "text" : "call to NewDecoder" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 377, + "startColumn" : 40, + "endColumn" : 48 + } + }, + "message" : { + "text" : "&..." + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 383, + "startColumn" : 8, + "endColumn" : 15 + } + }, + "message" : { + "text" : "gallery" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 196, + "startColumn" : 7, + "endColumn" : 8 + } + }, + "message" : { + "text" : "definition of g" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 198, + "startColumn" : 109, + "endColumn" : 116 + } + }, + "message" : { + "text" : "selection of Title" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 198, + "startColumn" : 11, + "endColumn" : 153 + } + }, + "message" : { + "text" : "call to Sprintf" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 200, + "startColumn" : 26, + "endColumn" : 31 + } + }, + "message" : { + "text" : "query" + } + } + } ] + } ] + } ], + "relatedLocations" : [ { + "id" : 1, + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 352, + "startColumn" : 17, + "endColumn" : 25 + } + }, + "message" : { + "text" : "user-provided value" + } + }, { + "id" : 2, + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 353, + "startColumn" : 18, + "endColumn" : 26 + } + }, + "message" : { + "text" : "user-provided value" + } + }, { + "id" : 3, + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 377, + "startColumn" : 25, + "endColumn" : 31 + } + }, + "message" : { + "text" : "user-provided value" + } + } ] + }, { + "ruleId" : "go/sql-injection", + "rule" : { + "id" : "go/sql-injection", + "index" : 8, + "toolComponent" : { + "index" : 0 + } + }, + "message" : { + "text" : "This query depends on a [user-provided value](1)." + }, + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 309, + "startColumn" : 26, + "endColumn" : 31 + } + } + } ], + "partialFingerprints" : { + "primaryLocationLineHash" : "cffaf601b82e3e2e:1", + "primaryLocationStartColumnFingerprint" : "24" + }, + "codeFlows" : [ { + "threadFlows" : [ { + "locations" : [ { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 541, + "startColumn" : 24, + "endColumn" : 30 + } + }, + "message" : { + "text" : "selection of Body" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 541, + "startColumn" : 8, + "endColumn" : 31 + } + }, + "message" : { + "text" : "call to NewDecoder" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 541, + "startColumn" : 39, + "endColumn" : 49 + } + }, + "message" : { + "text" : "&..." + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 549, + "startColumn" : 8, + "endColumn" : 17 + } + }, + "message" : { + "text" : "art_piece" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 305, + "startColumn" : 7, + "endColumn" : 8 + } + }, + "message" : { + "text" : "definition of p" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 308, + "startColumn" : 142, + "endColumn" : 149 + } + }, + "message" : { + "text" : "selection of Title" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 308, + "startColumn" : 11, + "endColumn" : 199 + } + }, + "message" : { + "text" : "call to Sprintf" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 309, + "startColumn" : 26, + "endColumn" : 31 + } + }, + "message" : { + "text" : "query" + } + } + } ] + } ] + }, { + "threadFlows" : [ { + "locations" : [ { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 541, + "startColumn" : 24, + "endColumn" : 30 + } + }, + "message" : { + "text" : "selection of Body" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 541, + "startColumn" : 8, + "endColumn" : 31 + } + }, + "message" : { + "text" : "call to NewDecoder" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 541, + "startColumn" : 39, + "endColumn" : 49 + } + }, + "message" : { + "text" : "&..." + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 549, + "startColumn" : 8, + "endColumn" : 17 + } + }, + "message" : { + "text" : "art_piece" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 305, + "startColumn" : 7, + "endColumn" : 8 + } + }, + "message" : { + "text" : "definition of p" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 308, + "startColumn" : 151, + "endColumn" : 164 + } + }, + "message" : { + "text" : "selection of Description" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 308, + "startColumn" : 11, + "endColumn" : 199 + } + }, + "message" : { + "text" : "call to Sprintf" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 309, + "startColumn" : 26, + "endColumn" : 31 + } + }, + "message" : { + "text" : "query" + } + } + } ] + } ] + }, { + "threadFlows" : [ { + "locations" : [ { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 541, + "startColumn" : 24, + "endColumn" : 30 + } + }, + "message" : { + "text" : "selection of Body" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 541, + "startColumn" : 8, + "endColumn" : 31 + } + }, + "message" : { + "text" : "call to NewDecoder" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 541, + "startColumn" : 39, + "endColumn" : 49 + } + }, + "message" : { + "text" : "&..." + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 549, + "startColumn" : 8, + "endColumn" : 17 + } + }, + "message" : { + "text" : "art_piece" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 305, + "startColumn" : 7, + "endColumn" : 8 + } + }, + "message" : { + "text" : "definition of p" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 308, + "startColumn" : 175, + "endColumn" : 180 + } + }, + "message" : { + "text" : "selection of Uri" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 308, + "startColumn" : 11, + "endColumn" : 199 + } + }, + "message" : { + "text" : "call to Sprintf" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 309, + "startColumn" : 26, + "endColumn" : 31 + } + }, + "message" : { + "text" : "query" + } + } + } ] + } ] + } ], + "relatedLocations" : [ { + "id" : 1, + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 541, + "startColumn" : 24, + "endColumn" : 30 + } + }, + "message" : { + "text" : "user-provided value" + } + } ] + }, { + "ruleId" : "go/clear-text-logging", + "rule" : { + "id" : "go/clear-text-logging", + "index" : 10, + "toolComponent" : { + "index" : 0 + } + }, + "message" : { + "text" : "[Sensitive data returned by HTTP request headers](1) flows to a logging call." + }, + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 660, + "startColumn" : 50, + "endColumn" : 61 + } + } + } ], + "partialFingerprints" : { + "primaryLocationLineHash" : "3154cb2d7c20d91d:1", + "primaryLocationStartColumnFingerprint" : "46" + }, + "codeFlows" : [ { + "threadFlows" : [ { + "locations" : [ { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 656, + "startColumn" : 12, + "endColumn" : 20 + } + }, + "message" : { + "text" : "selection of Header" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 656, + "startColumn" : 12, + "endColumn" : 41 + } + }, + "message" : { + "text" : "call to Get" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 656, + "startColumn" : 3, + "endColumn" : 8 + } + }, + "message" : { + "text" : "definition of authz" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 659, + "startColumn" : 56, + "endColumn" : 61 + } + }, + "message" : { + "text" : "authz" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 659, + "startColumn" : 37, + "endColumn" : 72 + } + }, + "message" : { + "text" : "call to TrimPrefix" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 659, + "startColumn" : 19, + "endColumn" : 73 + } + }, + "message" : { + "text" : "call to TrimSpace" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 659, + "startColumn" : 4, + "endColumn" : 15 + } + }, + "message" : { + "text" : "definition of tokenString" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 660, + "startColumn" : 50, + "endColumn" : 61 + } + }, + "message" : { + "text" : "tokenString" + } + } + } ] + } ] + } ], + "relatedLocations" : [ { + "id" : 1, + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 656, + "startColumn" : 12, + "endColumn" : 20 + } + }, + "message" : { + "text" : "Sensitive data returned by HTTP request headers" + } + } ] + }, { + "ruleId" : "go/clear-text-logging", + "rule" : { + "id" : "go/clear-text-logging", + "index" : 10, + "toolComponent" : { + "index" : 0 + } + }, + "message" : { + "text" : "[Sensitive data returned by HTTP request headers](1) flows to a logging call." + }, + "locations" : [ { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 678, + "startColumn" : 50, + "endColumn" : 55 + } + } + } ], + "partialFingerprints" : { + "primaryLocationLineHash" : "44645d539be4d0e4:1", + "primaryLocationStartColumnFingerprint" : "45" + }, + "codeFlows" : [ { + "threadFlows" : [ { + "locations" : [ { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 656, + "startColumn" : 12, + "endColumn" : 20 + } + }, + "message" : { + "text" : "selection of Header" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 656, + "startColumn" : 12, + "endColumn" : 41 + } + }, + "message" : { + "text" : "call to Get" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 656, + "startColumn" : 3, + "endColumn" : 8 + } + }, + "message" : { + "text" : "definition of authz" + } + } + }, { + "location" : { + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 678, + "startColumn" : 50, + "endColumn" : 55 + } + }, + "message" : { + "text" : "authz" + } + } + } ] + } ] + } ], + "relatedLocations" : [ { + "id" : 1, + "physicalLocation" : { + "artifactLocation" : { + "uri" : "gallery/main.go", + "uriBaseId" : "%SRCROOT%", + "index" : 0 + }, + "region" : { + "startLine" : 656, + "startColumn" : 12, + "endColumn" : 20 + } + }, + "message" : { + "text" : "Sensitive data returned by HTTP request headers" + } + } ] + } ], + "automationDetails" : { + "id" : "/language:go/" + }, + "columnKind" : "utf16CodeUnits", + "properties" : { + "codeqlConfigSummary" : { + "disableDefaultQueries" : false, + "queries" : [ { + "type" : "localQuery", + "uses" : "./queries/vue-xss.ql" + } ] + }, + "metricResults" : [ { + "rule" : { + "id" : "go/summary/lines-of-code", + "index" : 26, + "toolComponent" : { + "index" : 0 + } + }, + "ruleId" : "go/summary/lines-of-code", + "value" : 2019, + "baseline" : 2019 + } ], + "semmle.formatSpecifier" : "sarif-latest" + } + } ] +} \ No newline at end of file