diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8f13d8cb..3214a556 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,7 @@ --- name: CI on: [push] +permissions: read-all jobs: rebrand_check: diff --git a/CHANGELOG.md b/CHANGELOG.md index 96a60a72..4ec4a8ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,28 @@ All notable changes to WaboSign are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.3.1] — 2026-05-20 + +Security-focused patch addressing the alerts surfaced by the repo's first CodeQL scan (run against the 1.3.0 tag, commit [34250ac3](https://github.com/wabolabs/wabosign/commit/34250ac3)). No functional changes. + +### Security +- [app/views/submissions_filters/_filter_modal.html.erb](app/views/submissions_filters/_filter_modal.html.erb) — reflected XSS (`rb/reflected-xss`): `params[:path]` flowed unsanitised into both the form `action` and the "remove filter" link `href`. Now constrained via a `filter_path` local that defaults to `/` unless the supplied value starts with `/`, blocking `javascript:` and absolute-URL payloads. +- [app/controllers/start_form_controller.rb](app/controllers/start_form_controller.rb) — column-name injection (`rb/sql-injection`, two sites): `find_by!` / `find_or_initialize_by` were keyed by `required_params.except('name')`, whose keys derive from the template-owner-controlled `link_form_fields` preference. Replaced with `required_params.slice('email', 'phone')` so only the columns actually permitted by `submitter_params` can reach the SQL builder. +- [app/models/user.rb](app/models/user.rb) — ReDoS (`rb/redos`): the local-part of `FULL_EMAIL_REGEXP` used a nested quantifier (`(?:(?:[a-z0-9_-]+[.+'])*[a-z0-9_-]+)*`) that backtracks exponentially on adversarial input. Rewritten as `[a-z0-9_]+(?:[.'+\-][a-z0-9_]+)*` — same accepted set, linear matching. +- [app/controllers/mcp_controller.rb](app/controllers/mcp_controller.rb) — polynomial ReDoS (`rb/polynomial-redos`): Bearer-token extraction used `\ABearer\s+(.+)\z`, which CodeQL flags as polynomial on long Authorization headers. Replaced with a `start_with?('Bearer ')` check plus a string slice. +- [app/javascript/submission_form/dropzone.vue](app/javascript/submission_form/dropzone.vue), [initials_step.vue](app/javascript/submission_form/initials_step.vue), [signature_step.vue](app/javascript/submission_form/signature_step.vue) — insecure randomness (`js/insecure-randomness`): attachment-correlation UUIDs were generated with `Math.random().toString()`. Swapped to `crypto.randomUUID()`. The IDs are UI-only, but the change matches the secure default and clears the alerts. +- [.github/workflows/ci.yml](.github/workflows/ci.yml) — missing-workflow-permissions (`actions/missing-workflow-permissions`, six jobs): added a single workflow-level `permissions: read-all` block. All six CI jobs are read-only (lint/test/scan); none publish artefacts or post statuses that need write access. + +### Notes +- The following CodeQL alerts on the 1.3.0 commit are false positives in context and are not addressed by this release; they should be dismissed in the GitHub Security tab: + - `rb/insecure-mass-assignment` on the five settings controllers (`user_configs`, `storage_settings`, `email_smtp_settings`, `account_configs`, `account_custom_fields`) — every call site uses `params.require(...).permit(...)` strong-parameters before `update!`. + - `rb/csrf-protection-disabled` on `users/omniauth_callbacks_controller.rb` (OAuth provider callbacks legitimately can't carry a CSRF token) and `send_submission_email_controller.rb` (intentional public endpoint, rate-limited). + - `rb/weak-sensitive-data-hashing` on `preview_document_page_controller.rb`, `config/dotenv.rb`, `lib/puma/plugin/redis_server.rb` — SHA-1 is used only as a non-cryptographic identifier (tempfile path, cache key) and is not protecting sensitive data. + - `rb/clear-text-storage-sensitive-data` on `sso_settings_controller.rb` — the target column is on [`EncryptedConfig`](app/models/encrypted_config.rb), which declares `encrypts :value`, so the SSO `client_secret` is stored encrypted at rest. +- Released image: `ghcr.io/wabolabs/wabosign:1.3.1` (also tagged `:latest`). + +[1.3.1]: https://github.com/wabolabs/wabosign/releases/tag/1.3.1 + ## [1.3.0] — 2026-05-19 Adds three new SMS providers alongside the existing BulkVS integration. diff --git a/README.md b/README.md index 17cc4bb3..1bc55ef1 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ WaboSign is a fork of [DocuSeal](https://github.com/docusealco/docuseal) under A ## Docker ```sh -docker run --name wabosign -p 3000:3000 -v .:/data ghcr.io/wabolabs/wabosign:1.3.0 +docker run --name wabosign -p 3000:3000 -v .:/data ghcr.io/wabolabs/wabosign:1.3.1 ``` `:latest` always tracks the most recent release; pin a `MAJOR.MINOR.PATCH` tag for reproducible deployments. @@ -61,8 +61,8 @@ WaboSign ships with email + password (Devise) and TOTP two-factor auth out of th ## Releases -- **Current release:** 1.3.0 — see [CHANGELOG.md](CHANGELOG.md). -- **Container image:** `ghcr.io/wabolabs/wabosign:1.3.0` (or `:latest`). +- **Current release:** 1.3.1 — see [CHANGELOG.md](CHANGELOG.md). +- **Container image:** `ghcr.io/wabolabs/wabosign:1.3.1` (or `:latest`). - **Versioning:** `MAJOR.MINOR.PATCH` per [semver.org](https://semver.org). - **Tagging triggers a build:** pushing a `MAJOR.MINOR.PATCH` git tag runs [`.github/workflows/docker.yml`](.github/workflows/docker.yml), which builds `linux/amd64` + `linux/arm64` and pushes to GHCR. diff --git a/app/controllers/mcp_controller.rb b/app/controllers/mcp_controller.rb index 0ee39415..51bc165c 100644 --- a/app/controllers/mcp_controller.rb +++ b/app/controllers/mcp_controller.rb @@ -47,7 +47,8 @@ class McpController < ActionController::API end def user_from_api_key - token = request.headers['Authorization'].to_s[/\ABearer\s+(.+)\z/, 1] + auth = request.headers['Authorization'].to_s + token = auth.start_with?('Bearer ') ? auth[7..].strip.presence : nil return if token.blank? diff --git a/app/controllers/start_form_controller.rb b/app/controllers/start_form_controller.rb index c5adbab3..ca9cac5e 100644 --- a/app/controllers/start_form_controller.rb +++ b/app/controllers/start_form_controller.rb @@ -82,7 +82,7 @@ class StartFormController < ApplicationController @submitter = Submitter.where(submission: @template.submissions) .where.not(completed_at: nil) - .find_by!(required_params.except('name')) + .find_by!(required_params.slice('email', 'phone')) end private @@ -120,7 +120,7 @@ class StartFormController < ApplicationController required_params = required_fields.index_with { |key| submitter_params[key] } - find_params = required_params.except('name') + find_params = required_params.slice('email', 'phone') submitter = Submitter.new if find_params.compact_blank.blank? diff --git a/app/javascript/submission_form/dropzone.vue b/app/javascript/submission_form/dropzone.vue index 9a273f40..f85cace0 100644 --- a/app/javascript/submission_form/dropzone.vue +++ b/app/javascript/submission_form/dropzone.vue @@ -147,7 +147,7 @@ export default { reader.onloadend = () => { resolve({ url: reader.result, - uuid: Math.random().toString(), + uuid: crypto.randomUUID(), filename: file.name }) } diff --git a/app/javascript/submission_form/initials_step.vue b/app/javascript/submission_form/initials_step.vue index dde2f4e1..76b37eb1 100644 --- a/app/javascript/submission_form/initials_step.vue +++ b/app/javascript/submission_form/initials_step.vue @@ -412,7 +412,7 @@ export default { reader.readAsDataURL(file) reader.onloadend = () => { - const attachment = { url: reader.result, uuid: Math.random().toString() } + const attachment = { url: reader.result, uuid: crypto.randomUUID() } this.$emit('attached', attachment) this.$emit('update:model-value', attachment.uuid) diff --git a/app/javascript/submission_form/signature_step.vue b/app/javascript/submission_form/signature_step.vue index dc7cc18b..81695b18 100644 --- a/app/javascript/submission_form/signature_step.vue +++ b/app/javascript/submission_form/signature_step.vue @@ -910,7 +910,7 @@ export default { reader.readAsDataURL(file) reader.onloadend = () => { - const attachment = { url: reader.result, uuid: Math.random().toString() } + const attachment = { url: reader.result, uuid: crypto.randomUUID() } this.$emit('attached', attachment) this.$emit('update:model-value', attachment.uuid) diff --git a/app/models/user.rb b/app/models/user.rb index 9a81355e..8b64a23d 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -56,7 +56,7 @@ class User < ApplicationRecord EMAIL_REGEXP = /[^@;,<>\s]+@[^@;,<>\s]+/ FULL_EMAIL_REGEXP = - /\A[a-z0-9][.']?(?:(?:[a-z0-9_-]+[.+'])*[a-z0-9_-]+)*@(?:[a-z0-9]+[.-])*[a-z0-9]+\.[a-z]{2,}\z/i + /\A[a-z0-9_]+(?:[.'+\-][a-z0-9_]+)*@(?:[a-z0-9]+[.\-])*[a-z0-9]+\.[a-z]{2,}\z/i has_one_attached :signature has_one_attached :initials diff --git a/app/views/submissions_filters/_filter_modal.html.erb b/app/views/submissions_filters/_filter_modal.html.erb index 8794197a..d066015a 100644 --- a/app/views/submissions_filters/_filter_modal.html.erb +++ b/app/views/submissions_filters/_filter_modal.html.erb @@ -1,5 +1,6 @@ +<% filter_path = params[:path].to_s.start_with?('/') ? params[:path] : '/' %> <%= render 'shared/turbo_modal', title: local_assigns[:title] do %> - <%= form_for '', url: params[:path], method: :get, data: { turbo_frame: :_top }, html: { autocomplete: :off } do |f| %> + <%= form_for '', url: filter_path, method: :get, data: { turbo_frame: :_top }, html: { autocomplete: :off } do |f| %> <%= hidden_field_tag :q, params[:q] if params[:q].present? %> <% local_assigns[:default_params].each do |key, value| %> <%= hidden_field_tag(key, value) if value.present? %> @@ -10,7 +11,7 @@ <% if params[:with_remove] %>