* Security fixes for admin and sendmail

Because the Vaultwarden Admin Backend endpoints did not validated the Content-Type during a request, it was possible to update settings via CSRF. But, this was only possible if there was no `ADMIN_TOKEN` set at all. To make sure these environments are also safe I added the needed content-type checks at the functions.
This could cause some users who have scripts which uses cURL for example to adjust there commands to provide the correct headers.

By using a crafted favicon and having access to the Admin Backend an attacker could run custom commands on the host/container where Vaultwarden is running on. The main issue here is that we allowed the sendmail binary name/path to be changed. To mitigate this we removed this configuration item and only then `sendmail` binary as a name can be used.
This could cause some issues where the `sendmail` binary is not in the `$PATH` and thus not able to be started. In these cases the admins should make sure `$PATH` is set correctly or create a custom shell script or symlink at a location which is in the `$PATH`.

Added an extra security header and adjusted the CSP to be more strict by setting `default-src` to `none` and added the needed missing specific policies.

Also created a general email validation function which does some more checking to catch invalid email address not found by the email_address crate.

Signed-off-by: BlackDex <black.dex@gmail.com>

* Fix security issue with organizationId validation

Because of a invalid check/validation of the OrganizationId which most of the time is located in the path but sometimes provided as a URL Parameter, the parameter overruled the path ID during the Guard checks.
This resulted in someone being able to execute commands as an Admin or Owner of the OrganizationId fetched from the parameter, but the API endpoints then used the OrganizationId located in the path instead.

This commit fixes the extraction of the OrganizationId in the Guard and also added some extra validations of this OrgId in several functions.

Also added an extra `OrgMemberHeaders` which can be used to only allow access to organization endpoints which should only be accessible by members of that org.

Signed-off-by: BlackDex <black.dex@gmail.com>

* Update server version in config endpoint

Updated the server version reported to the clients to `2025.1.0`.
This should make Vaultwarden future proof for the newer clients released by Bitwarden.

Signed-off-by: BlackDex <black.dex@gmail.com>

* Fix and adjust build workflow

The build workflow had an issue with some `if` checks.
For one they had two `$` signs, and it is not recommended to use `always()` since canceling a workflow does not cancel those calls.
Using `!cancelled()` is the preferred way.

Signed-off-by: BlackDex <black.dex@gmail.com>

* Update crates

Signed-off-by: BlackDex <black.dex@gmail.com>

* Allow sendmail to be configurable

This reverts a previous change which removed the sendmail to be configurable.
We now set the config to be read-only, and omit all read-only values from being stored during a save action from the admin interface.

Signed-off-by: BlackDex <black.dex@gmail.com>

* Add more org_id checks

Added more org_id checks at all functions which use the org_id in there path.

Signed-off-by: BlackDex <black.dex@gmail.com>

---------

Signed-off-by: BlackDex <black.dex@gmail.com>
This commit is contained in:
Mathijs van Veluw 2025-01-25 01:32:09 +01:00 committed by GitHub
parent c0be36a17f
commit 952992c85b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 418 additions and 223 deletions

View File

@ -120,37 +120,37 @@ jobs:
# First test all features together, afterwards test them separately.
- name: "test features: sqlite,mysql,postgresql,enable_mimalloc,query_logger"
id: test_sqlite_mysql_postgresql_mimalloc_logger
if: $${{ always() }}
if: ${{ !cancelled() }}
run: |
cargo test --features sqlite,mysql,postgresql,enable_mimalloc,query_logger
- name: "test features: sqlite,mysql,postgresql,enable_mimalloc"
id: test_sqlite_mysql_postgresql_mimalloc
if: $${{ always() }}
if: ${{ !cancelled() }}
run: |
cargo test --features sqlite,mysql,postgresql,enable_mimalloc
- name: "test features: sqlite,mysql,postgresql"
id: test_sqlite_mysql_postgresql
if: $${{ always() }}
if: ${{ !cancelled() }}
run: |
cargo test --features sqlite,mysql,postgresql
- name: "test features: sqlite"
id: test_sqlite
if: $${{ always() }}
if: ${{ !cancelled() }}
run: |
cargo test --features sqlite
- name: "test features: mysql"
id: test_mysql
if: $${{ always() }}
if: ${{ !cancelled() }}
run: |
cargo test --features mysql
- name: "test features: postgresql"
id: test_postgresql
if: $${{ always() }}
if: ${{ !cancelled() }}
run: |
cargo test --features postgresql
# End Run cargo tests
@ -159,7 +159,7 @@ jobs:
# Run cargo clippy, and fail on warnings
- name: "clippy features: sqlite,mysql,postgresql,enable_mimalloc"
id: clippy
if: ${{ always() && matrix.channel == 'rust-toolchain' }}
if: ${{ !cancelled() && matrix.channel == 'rust-toolchain' }}
run: |
cargo clippy --features sqlite,mysql,postgresql,enable_mimalloc -- -D warnings
# End Run cargo clippy
@ -168,7 +168,7 @@ jobs:
# Run cargo fmt (Only run on rust-toolchain defined version)
- name: "check formatting"
id: formatting
if: ${{ always() && matrix.channel == 'rust-toolchain' }}
if: ${{ !cancelled() && matrix.channel == 'rust-toolchain' }}
run: |
cargo fmt --all -- --check
# End Run cargo fmt

164
Cargo.lock generated
View File

@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
version = 4
[[package]]
name = "addr2line"
@ -371,9 +371,9 @@ checksum = "383d29d513d8764dcdc42ea295d979eb99c3c9f00607b3692cf68a431f7dca72"
[[package]]
name = "bitflags"
version = "2.6.0"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36"
[[package]]
name = "blake2"
@ -419,9 +419,9 @@ dependencies = [
[[package]]
name = "brotli-decompressor"
version = "4.0.1"
version = "4.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a45bd2e4095a8b518033b128020dd4a55aab1c0a381ba4404a472630f4bc362"
checksum = "74fa05ad7d803d413eb8380983b092cbbaf9a85f151b871360e7b00cd7060b37"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
@ -489,9 +489,9 @@ checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0"
[[package]]
name = "cc"
version = "1.2.7"
version = "1.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a012a0df96dd6d06ba9a1b29d6402d1a5d77c6befd2566afdc26e10603dc93d7"
checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229"
dependencies = [
"shlex",
]
@ -517,9 +517,9 @@ dependencies = [
[[package]]
name = "chrono-tz"
version = "0.10.0"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd6dd8046d00723a59a2f8c5f295c515b9bb9a331ee4f8f3d4dd49e428acd3b6"
checksum = "9c6ac4f2c0bf0f44e9161aec9675e1050aa4a530663c4a9e37e108fa948bca9f"
dependencies = [
"chrono",
"chrono-tz-build",
@ -702,9 +702,9 @@ dependencies = [
[[package]]
name = "data-encoding"
version = "2.6.0"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2"
checksum = "0e60eed09d8c01d3cee5b7d30acb059b76614c918fa0f992e0dd6eeb10daad6f"
[[package]]
name = "data-url"
@ -1152,9 +1152,9 @@ checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
[[package]]
name = "futures-lite"
version = "2.5.0"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cef40d21ae2c515b51041df9ed313ed21e572df340ea58a922a0aefe7e8891a1"
checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532"
dependencies = [
"fastrand",
"futures-core",
@ -1342,7 +1342,7 @@ dependencies = [
"pest_derive",
"serde",
"serde_json",
"thiserror 2.0.9",
"thiserror 2.0.11",
"walkdir",
]
@ -1586,7 +1586,7 @@ dependencies = [
"http 1.2.0",
"hyper 1.5.2",
"hyper-util",
"rustls 0.23.20",
"rustls 0.23.21",
"rustls-pki-types",
"tokio",
"tokio-rustls 0.26.1",
@ -1798,9 +1798,9 @@ dependencies = [
[[package]]
name = "indexmap"
version = "2.7.0"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f"
checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652"
dependencies = [
"equivalent",
"hashbrown 0.15.2",
@ -1827,19 +1827,19 @@ dependencies = [
[[package]]
name = "ipnet"
version = "2.10.1"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708"
checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
[[package]]
name = "is-terminal"
version = "0.4.13"
version = "0.4.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b"
checksum = "e19b23d53f35ce9f56aebc7d1bb4e6ac1e9c0db7ac85c8d1760c04379edced37"
dependencies = [
"hermit-abi 0.4.0",
"libc",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@ -1867,9 +1867,9 @@ dependencies = [
[[package]]
name = "js-sys"
version = "0.3.76"
version = "0.3.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7"
checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
dependencies = [
"once_cell",
"wasm-bindgen",
@ -2014,9 +2014,9 @@ dependencies = [
[[package]]
name = "log"
version = "0.4.22"
version = "0.4.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f"
dependencies = [
"value-bag",
]
@ -2118,9 +2118,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniz_oxide"
version = "0.8.2"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394"
checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924"
dependencies = [
"adler2",
]
@ -2336,9 +2336,9 @@ dependencies = [
[[package]]
name = "openssl-probe"
version = "0.1.5"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
[[package]]
name = "openssl-src"
@ -2469,7 +2469,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc"
dependencies = [
"memchr",
"thiserror 2.0.9",
"thiserror 2.0.11",
"ucd-trie",
]
@ -2641,9 +2641,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.92"
version = "1.0.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0"
checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99"
dependencies = [
"unicode-ident",
]
@ -2765,9 +2765,9 @@ dependencies = [
[[package]]
name = "raw-cpuid"
version = "11.2.0"
version = "11.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ab240315c661615f2ee9f0f2cd32d5a7343a84d5ebcccb99d46e6637565e7b0"
checksum = "c6928fa44c097620b706542d428957635951bade7143269085389d42c8a4927e"
dependencies = [
"bitflags",
]
@ -3077,9 +3077,9 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "rustix"
version = "0.38.43"
version = "0.38.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6"
checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
dependencies = [
"bitflags",
"errno",
@ -3102,9 +3102,9 @@ dependencies = [
[[package]]
name = "rustls"
version = "0.23.20"
version = "0.23.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5065c3f250cbd332cd894be57c40fa52387247659b14a2d6041d121547903b1b"
checksum = "8f287924602bf649d949c63dc8ac8b235fa5387d394020705b80c4eb597ce5b8"
dependencies = [
"once_cell",
"rustls-pki-types",
@ -3244,9 +3244,9 @@ dependencies = [
[[package]]
name = "semver"
version = "1.0.24"
version = "1.0.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba"
checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03"
[[package]]
name = "serde"
@ -3280,9 +3280,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.135"
version = "1.0.137"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9"
checksum = "930cfb6e6abf99298aaad7d29abbef7a9999a9a8806a40088f55f0dcec03146b"
dependencies = [
"itoa",
"memchr",
@ -3369,13 +3369,13 @@ dependencies = [
[[package]]
name = "simple_asn1"
version = "0.6.2"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085"
checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb"
dependencies = [
"num-bigint",
"num-traits",
"thiserror 1.0.69",
"thiserror 2.0.11",
"time",
]
@ -3476,9 +3476,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "2.0.95"
version = "2.0.96"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46f71c0377baf4ef1cc3e3402ded576dccc315800fbc62dfc7fe04b009773b4a"
checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80"
dependencies = [
"proc-macro2",
"quote",
@ -3563,11 +3563,11 @@ dependencies = [
[[package]]
name = "thiserror"
version = "2.0.9"
version = "2.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc"
checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc"
dependencies = [
"thiserror-impl 2.0.9",
"thiserror-impl 2.0.11",
]
[[package]]
@ -3583,9 +3583,9 @@ dependencies = [
[[package]]
name = "thiserror-impl"
version = "2.0.9"
version = "2.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4"
checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2"
dependencies = [
"proc-macro2",
"quote",
@ -3671,9 +3671,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.42.0"
version = "1.43.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551"
checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e"
dependencies = [
"backtrace",
"bytes",
@ -3689,9 +3689,9 @@ dependencies = [
[[package]]
name = "tokio-macros"
version = "2.4.0"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
dependencies = [
"proc-macro2",
"quote",
@ -3724,7 +3724,7 @@ version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37"
dependencies = [
"rustls 0.23.20",
"rustls 0.23.21",
"tokio",
]
@ -3969,9 +3969,9 @@ dependencies = [
[[package]]
name = "unicode-ident"
version = "1.0.14"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
checksum = "11cd88e12b17c6494200a9c1b683a04fcac9573ed74cd1b62aeb2727c5592243"
[[package]]
name = "unicode-xid"
@ -4017,18 +4017,18 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "uuid"
version = "1.11.0"
version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
checksum = "b3758f5e68192bb96cc8f9b7e2c2cfdabb435499a28499a42f8f984092adad4b"
dependencies = [
"getrandom",
]
[[package]]
name = "valuable"
version = "0.1.0"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]]
name = "value-bag"
@ -4141,20 +4141,21 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.99"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396"
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.99"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79"
checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
dependencies = [
"bumpalo",
"log",
@ -4166,9 +4167,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.49"
version = "0.4.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2"
checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61"
dependencies = [
"cfg-if",
"js-sys",
@ -4179,9 +4180,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.99"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe"
checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@ -4189,9 +4190,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.99"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2"
checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
dependencies = [
"proc-macro2",
"quote",
@ -4202,9 +4203,12 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.99"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6"
checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
dependencies = [
"unicode-ident",
]
[[package]]
name = "wasm-streams"
@ -4221,9 +4225,9 @@ dependencies = [
[[package]]
name = "web-sys"
version = "0.3.76"
version = "0.3.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc"
checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2"
dependencies = [
"js-sys",
"wasm-bindgen",
@ -4515,9 +4519,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.6.22"
version = "0.6.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39281189af81c07ec09db316b302a3e67bf9bd7cbf6c820b50e35fee9c2fa980"
checksum = "c8d71a593cc5c42ad7876e2c1fda56f314f3754c084128833e64f1345ff8a03a"
dependencies = [
"memchr",
]

View File

@ -44,7 +44,7 @@ syslog = "7.0.0"
macros = { path = "./macros" }
# Logging
log = "0.4.22"
log = "0.4.25"
fern = { version = "0.7.1", features = ["syslog-7", "reopen-1"] }
tracing = { version = "0.1.41", features = ["log"] } # Needed to have lettre and webauthn-rs trace logging to work
@ -71,11 +71,11 @@ dashmap = "6.1.0"
# Async futures
futures = "0.3.31"
tokio = { version = "1.42.0", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] }
tokio = { version = "1.43.0", features = ["rt-multi-thread", "fs", "io-util", "parking_lot", "time", "signal", "net"] }
# A generic serialization/deserialization framework
serde = { version = "1.0.217", features = ["derive"] }
serde_json = "1.0.135"
serde_json = "1.0.137"
# A safe, extensible ORM and Query builder
diesel = { version = "2.2.6", features = ["chrono", "r2d2", "numeric"] }
@ -93,18 +93,18 @@ rand = { version = "0.8.5", features = ["small_rng"] }
ring = "0.17.8"
# UUID generation
uuid = { version = "1.11.0", features = ["v4"] }
uuid = { version = "1.12.1", features = ["v4"] }
# Date and time libraries
chrono = { version = "0.4.39", features = ["clock", "serde"], default-features = false }
chrono-tz = "0.10.0"
chrono-tz = "0.10.1"
time = "0.3.37"
# Job scheduler
job_scheduler_ng = "2.0.5"
# Data encoding library Hex/Base32/Base64
data-encoding = "2.6.0"
data-encoding = "2.7.0"
# JWT library
jsonwebtoken = "9.3.0"
@ -157,7 +157,7 @@ paste = "1.0.15"
governor = "0.8.0"
# Check client versions for specific features.
semver = "1.0.24"
semver = "1.0.25"
# Allow overriding the default memory allocator
# Mainly used for the musl builds, since the default musl malloc is very slow

View File

@ -10,4 +10,4 @@ proc-macro = true
[dependencies]
quote = "1.0.38"
syn = "2.0.94"
syn = "2.0.96"

View File

@ -171,7 +171,7 @@ struct LoginForm {
redirect: Option<String>,
}
#[post("/", data = "<data>")]
#[post("/", format = "application/x-www-form-urlencoded", data = "<data>")]
fn post_admin_login(
data: Form<LoginForm>,
cookies: &CookieJar<'_>,
@ -289,7 +289,7 @@ async fn get_user_or_404(user_id: &UserId, conn: &mut DbConn) -> ApiResult<User>
}
}
#[post("/invite", data = "<data>")]
#[post("/invite", format = "application/json", data = "<data>")]
async fn invite_user(data: Json<InviteData>, _token: AdminToken, mut conn: DbConn) -> JsonResult {
let data: InviteData = data.into_inner();
if User::find_by_mail(&data.email, &mut conn).await.is_some() {
@ -315,7 +315,7 @@ async fn invite_user(data: Json<InviteData>, _token: AdminToken, mut conn: DbCon
Ok(Json(user.to_json(&mut conn).await))
}
#[post("/test/smtp", data = "<data>")]
#[post("/test/smtp", format = "application/json", data = "<data>")]
async fn test_smtp(data: Json<InviteData>, _token: AdminToken) -> EmptyResult {
let data: InviteData = data.into_inner();
@ -393,7 +393,7 @@ async fn get_user_json(user_id: UserId, _token: AdminToken, mut conn: DbConn) ->
Ok(Json(usr))
}
#[post("/users/<user_id>/delete")]
#[post("/users/<user_id>/delete", format = "application/json")]
async fn delete_user(user_id: UserId, token: AdminToken, mut conn: DbConn) -> EmptyResult {
let user = get_user_or_404(&user_id, &mut conn).await?;
@ -417,7 +417,7 @@ async fn delete_user(user_id: UserId, token: AdminToken, mut conn: DbConn) -> Em
res
}
#[post("/users/<user_id>/deauth")]
#[post("/users/<user_id>/deauth", format = "application/json")]
async fn deauth_user(user_id: UserId, _token: AdminToken, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
let mut user = get_user_or_404(&user_id, &mut conn).await?;
@ -438,7 +438,7 @@ async fn deauth_user(user_id: UserId, _token: AdminToken, mut conn: DbConn, nt:
user.save(&mut conn).await
}
#[post("/users/<user_id>/disable")]
#[post("/users/<user_id>/disable", format = "application/json")]
async fn disable_user(user_id: UserId, _token: AdminToken, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
let mut user = get_user_or_404(&user_id, &mut conn).await?;
Device::delete_all_by_user(&user.uuid, &mut conn).await?;
@ -452,7 +452,7 @@ async fn disable_user(user_id: UserId, _token: AdminToken, mut conn: DbConn, nt:
save_result
}
#[post("/users/<user_id>/enable")]
#[post("/users/<user_id>/enable", format = "application/json")]
async fn enable_user(user_id: UserId, _token: AdminToken, mut conn: DbConn) -> EmptyResult {
let mut user = get_user_or_404(&user_id, &mut conn).await?;
user.enabled = true;
@ -460,7 +460,7 @@ async fn enable_user(user_id: UserId, _token: AdminToken, mut conn: DbConn) -> E
user.save(&mut conn).await
}
#[post("/users/<user_id>/remove-2fa")]
#[post("/users/<user_id>/remove-2fa", format = "application/json")]
async fn remove_2fa(user_id: UserId, token: AdminToken, mut conn: DbConn) -> EmptyResult {
let mut user = get_user_or_404(&user_id, &mut conn).await?;
TwoFactor::delete_all_by_user(&user.uuid, &mut conn).await?;
@ -469,7 +469,7 @@ async fn remove_2fa(user_id: UserId, token: AdminToken, mut conn: DbConn) -> Emp
user.save(&mut conn).await
}
#[post("/users/<user_id>/invite/resend")]
#[post("/users/<user_id>/invite/resend", format = "application/json")]
async fn resend_user_invite(user_id: UserId, _token: AdminToken, mut conn: DbConn) -> EmptyResult {
if let Some(user) = User::find_by_uuid(&user_id, &mut conn).await {
//TODO: replace this with user.status check when it will be available (PR#3397)
@ -496,7 +496,7 @@ struct MembershipTypeData {
org_uuid: OrganizationId,
}
#[post("/users/org_type", data = "<data>")]
#[post("/users/org_type", format = "application/json", data = "<data>")]
async fn update_membership_type(data: Json<MembershipTypeData>, token: AdminToken, mut conn: DbConn) -> EmptyResult {
let data: MembershipTypeData = data.into_inner();
@ -550,7 +550,7 @@ async fn update_membership_type(data: Json<MembershipTypeData>, token: AdminToke
member_to_edit.save(&mut conn).await
}
#[post("/users/update_revision")]
#[post("/users/update_revision", format = "application/json")]
async fn update_revision_users(_token: AdminToken, mut conn: DbConn) -> EmptyResult {
User::update_all_revisions(&mut conn).await
}
@ -575,7 +575,7 @@ async fn organizations_overview(_token: AdminToken, mut conn: DbConn) -> ApiResu
Ok(Html(text))
}
#[post("/organizations/<org_id>/delete")]
#[post("/organizations/<org_id>/delete", format = "application/json")]
async fn delete_organization(org_id: OrganizationId, _token: AdminToken, mut conn: DbConn) -> EmptyResult {
let org = Organization::find_by_uuid(&org_id, &mut conn).await.map_res("Organization doesn't exist")?;
org.delete(&mut conn).await
@ -733,7 +733,7 @@ async fn diagnostics(_token: AdminToken, ip_header: IpHeader, mut conn: DbConn)
Ok(Html(text))
}
#[get("/diagnostics/config")]
#[get("/diagnostics/config", format = "application/json")]
fn get_diagnostics_config(_token: AdminToken) -> Json<Value> {
let support_json = CONFIG.get_support_json();
Json(support_json)
@ -744,16 +744,16 @@ fn get_diagnostics_http(code: u16, _token: AdminToken) -> EmptyResult {
err_code!(format!("Testing error {code} response"), code);
}
#[post("/config", data = "<data>")]
#[post("/config", format = "application/json", data = "<data>")]
fn post_config(data: Json<ConfigBuilder>, _token: AdminToken) -> EmptyResult {
let data: ConfigBuilder = data.into_inner();
if let Err(e) = CONFIG.update_config(data) {
if let Err(e) = CONFIG.update_config(data, true) {
err!(format!("Unable to save config: {e:?}"))
}
Ok(())
}
#[post("/config/delete")]
#[post("/config/delete", format = "application/json")]
fn delete_config(_token: AdminToken) -> EmptyResult {
if let Err(e) = CONFIG.delete_user_config() {
err!(format!("Unable to delete config: {e:?}"))
@ -761,7 +761,7 @@ fn delete_config(_token: AdminToken) -> EmptyResult {
Ok(())
}
#[post("/config/backup_db")]
#[post("/config/backup_db", format = "application/json")]
async fn backup_db(_token: AdminToken, mut conn: DbConn) -> ApiResult<String> {
if *CAN_BACKUP {
match backup_database(&mut conn).await {

View File

@ -34,9 +34,13 @@ struct EventRange {
async fn get_org_events(
org_id: OrganizationId,
data: EventRange,
_headers: AdminHeaders,
headers: AdminHeaders,
mut conn: DbConn,
) -> JsonResult {
if org_id != headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
// Return an empty vec when we org events are disabled.
// This prevents client errors
let events_json: Vec<Value> = if !CONFIG.org_events_enabled() {
@ -100,9 +104,12 @@ async fn get_user_events(
org_id: OrganizationId,
member_id: MembershipId,
data: EventRange,
_headers: AdminHeaders,
headers: AdminHeaders,
mut conn: DbConn,
) -> JsonResult {
if org_id != headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
// Return an empty vec when we org events are disabled.
// This prevents client errors
let events_json: Vec<Value> = if !CONFIG.org_events_enabled() {

View File

@ -210,8 +210,8 @@ fn config() -> Json<Value> {
// This means they expect a version that closely matches the Bitwarden server version
// We should make sure that we keep this updated when we support the new server features
// Version history:
// - Individual cipher key encryption: 2023.9.1
"version": "2024.2.0",
// - Individual cipher key encryption: 2024.2.0
"version": "2025.1.0",
"gitHash": option_env!("GIT_REV"),
"server": {
"name": "Vaultwarden",

View File

@ -10,7 +10,10 @@ use crate::{
core::{log_event, two_factor, CipherSyncData, CipherSyncType},
EmptyResult, JsonResult, Notify, PasswordOrOtpData, UpdateType,
},
auth::{decode_invite, AdminHeaders, ClientVersion, Headers, ManagerHeaders, ManagerHeadersLoose, OwnerHeaders},
auth::{
decode_invite, AdminHeaders, ClientVersion, Headers, ManagerHeaders, ManagerHeadersLoose, OrgMemberHeaders,
OwnerHeaders,
},
db::{models::*, DbConn},
mail,
util::{convert_json_key_lcase_first, NumberOrString},
@ -213,6 +216,9 @@ async fn delete_organization(
headers: OwnerHeaders,
mut conn: DbConn,
) -> EmptyResult {
if org_id != headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
let data: PasswordOrOtpData = data.into_inner();
data.validate(&headers.user, true, &mut conn).await?;
@ -261,7 +267,10 @@ async fn leave_organization(org_id: OrganizationId, headers: Headers, mut conn:
}
#[get("/organizations/<org_id>")]
async fn get_organization(org_id: OrganizationId, _headers: OwnerHeaders, mut conn: DbConn) -> JsonResult {
async fn get_organization(org_id: OrganizationId, headers: OwnerHeaders, mut conn: DbConn) -> JsonResult {
if org_id != headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
match Organization::find_by_uuid(&org_id, &mut conn).await {
Some(organization) => Ok(Json(organization.to_json())),
None => err!("Can't find organization details"),
@ -285,14 +294,18 @@ async fn post_organization(
data: Json<OrganizationUpdateData>,
mut conn: DbConn,
) -> JsonResult {
if org_id != headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
let data: OrganizationUpdateData = data.into_inner();
let Some(mut org) = Organization::find_by_uuid(&org_id, &mut conn).await else {
err!("Can't find organization details")
err!("Organization not found")
};
org.name = data.name;
org.billing_email = data.billing_email;
org.billing_email = data.billing_email.to_lowercase();
org.save(&mut conn).await?;
@ -325,12 +338,15 @@ async fn get_user_collections(headers: Headers, mut conn: DbConn) -> Json<Value>
}
#[get("/organizations/<org_id>/collections")]
async fn get_org_collections(org_id: OrganizationId, _headers: ManagerHeadersLoose, mut conn: DbConn) -> Json<Value> {
Json(json!({
async fn get_org_collections(org_id: OrganizationId, headers: ManagerHeadersLoose, mut conn: DbConn) -> JsonResult {
if org_id != headers.membership.org_uuid {
err!("Organization not found", "Organization id's do not match");
}
Ok(Json(json!({
"data": _get_org_collections(&org_id, &mut conn).await,
"object": "list",
"continuationToken": null,
}))
})))
}
#[get("/organizations/<org_id>/collections/details")]
@ -339,6 +355,9 @@ async fn get_org_collections_details(
headers: ManagerHeadersLoose,
mut conn: DbConn,
) -> JsonResult {
if org_id != headers.membership.org_uuid {
err!("Organization not found", "Organization id's do not match");
}
let mut data = Vec::new();
let Some(member) = Membership::find_by_user_and_org(&headers.user.uuid, &org_id, &mut conn).await else {
@ -413,6 +432,9 @@ async fn post_organization_collections(
data: Json<NewCollectionData>,
mut conn: DbConn,
) -> JsonResult {
if org_id != headers.membership.org_uuid {
err!("Organization not found", "Organization id's do not match");
}
let data: NewCollectionData = data.into_inner();
let Some(org) = Organization::find_by_uuid(&org_id, &mut conn).await else {
@ -485,6 +507,9 @@ async fn post_organization_collection_update(
data: Json<NewCollectionData>,
mut conn: DbConn,
) -> JsonResult {
if org_id != headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
let data: NewCollectionData = data.into_inner();
if Organization::find_by_uuid(&org_id, &mut conn).await.is_none() {
@ -545,9 +570,12 @@ async fn delete_organization_collection_member(
org_id: OrganizationId,
col_id: CollectionId,
member_id: MembershipId,
_headers: AdminHeaders,
headers: AdminHeaders,
mut conn: DbConn,
) -> EmptyResult {
if org_id != headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
let Some(collection) = Collection::find_by_uuid_and_org(&col_id, &org_id, &mut conn).await else {
err!("Collection not found", "Collection does not exist or does not belong to this organization")
};
@ -638,6 +666,9 @@ async fn bulk_delete_organization_collections(
data: Json<BulkCollectionIds>,
mut conn: DbConn,
) -> EmptyResult {
if org_id != headers.membership.org_uuid {
err!("Organization not found", "Organization id's do not match");
}
let data: BulkCollectionIds = data.into_inner();
let collections = data.ids;
@ -657,6 +688,9 @@ async fn get_org_collection_detail(
headers: ManagerHeaders,
mut conn: DbConn,
) -> JsonResult {
if org_id != headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
match Collection::find_by_uuid_and_user(&col_id, headers.user.uuid.clone(), &mut conn).await {
None => err!("Collection not found"),
Some(collection) => {
@ -718,9 +752,12 @@ async fn get_org_collection_detail(
async fn get_collection_users(
org_id: OrganizationId,
col_id: CollectionId,
_headers: ManagerHeaders,
headers: ManagerHeaders,
mut conn: DbConn,
) -> JsonResult {
if org_id != headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
// Get org and collection, check that collection is from org
let Some(collection) = Collection::find_by_uuid_and_org(&col_id, &org_id, &mut conn).await else {
err!("Collection not found in Organization")
@ -744,9 +781,12 @@ async fn put_collection_users(
org_id: OrganizationId,
col_id: CollectionId,
data: Json<Vec<MembershipData>>,
_headers: ManagerHeaders,
headers: ManagerHeaders,
mut conn: DbConn,
) -> EmptyResult {
if org_id != headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
// Get org and collection, check that collection is from org
if Collection::find_by_uuid_and_org(&col_id, &org_id, &mut conn).await.is_none() {
err!("Collection not found in Organization")
@ -778,10 +818,9 @@ struct OrgIdData {
}
#[get("/ciphers/organization-details?<data..>")]
async fn get_org_details(data: OrgIdData, headers: Headers, mut conn: DbConn) -> JsonResult {
if Membership::find_confirmed_by_user_and_org(&headers.user.uuid, &data.organization_id, &mut conn).await.is_none()
{
err_code!("Resource not found.", rocket::http::Status::NotFound.code);
async fn get_org_details(data: OrgIdData, headers: OrgMemberHeaders, mut conn: DbConn) -> JsonResult {
if data.organization_id != headers.org_id {
err_code!("Resource not found.", "Organization id's do not match", rocket::http::Status::NotFound.code);
}
Ok(Json(json!({
@ -814,9 +853,12 @@ struct GetOrgUserData {
async fn get_members(
data: GetOrgUserData,
org_id: OrganizationId,
_headers: ManagerHeadersLoose,
headers: ManagerHeadersLoose,
mut conn: DbConn,
) -> Json<Value> {
) -> JsonResult {
if org_id != headers.membership.org_uuid {
err!("Organization not found", "Organization id's do not match");
}
let mut users_json = Vec::new();
for u in Membership::find_by_org(&org_id, &mut conn).await {
users_json.push(
@ -829,20 +871,23 @@ async fn get_members(
);
}
Json(json!({
Ok(Json(json!({
"data": users_json,
"object": "list",
"continuationToken": null,
}))
})))
}
#[post("/organizations/<org_id>/keys", data = "<data>")]
async fn post_org_keys(
org_id: OrganizationId,
data: Json<OrgKeyData>,
_headers: AdminHeaders,
headers: AdminHeaders,
mut conn: DbConn,
) -> JsonResult {
if org_id != headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
let data: OrgKeyData = data.into_inner();
let mut org = match Organization::find_by_uuid(&org_id, &mut conn).await {
@ -905,6 +950,9 @@ async fn send_invite(
headers: AdminHeaders,
mut conn: DbConn,
) -> EmptyResult {
if org_id != headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
let mut data: InviteData = data.into_inner();
// HACK: We need the raw user-type to be sure custom role is selected to determine the access_all permission
@ -1045,7 +1093,10 @@ async fn bulk_reinvite_members(
data: Json<BulkMembershipIds>,
headers: AdminHeaders,
mut conn: DbConn,
) -> Json<Value> {
) -> JsonResult {
if org_id != headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
let data: BulkMembershipIds = data.into_inner();
let mut bulk_response = Vec::new();
@ -1064,11 +1115,11 @@ async fn bulk_reinvite_members(
))
}
Json(json!({
Ok(Json(json!({
"data": bulk_response,
"object": "list",
"continuationToken": null
}))
})))
}
#[post("/organizations/<org_id>/users/<member_id>/reinvite")]
@ -1237,7 +1288,10 @@ async fn bulk_confirm_invite(
headers: AdminHeaders,
mut conn: DbConn,
nt: Notify<'_>,
) -> Json<Value> {
) -> JsonResult {
if org_id != headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
let data = data.into_inner();
let mut bulk_response = Vec::new();
@ -1263,11 +1317,11 @@ async fn bulk_confirm_invite(
None => error!("No keys to confirm"),
}
Json(json!({
Ok(Json(json!({
"data": bulk_response,
"object": "list",
"continuationToken": null
}))
})))
}
#[post("/organizations/<org_id>/users/<member_id>/confirm", data = "<data>")]
@ -1364,19 +1418,22 @@ async fn _confirm_invite(
#[get("/organizations/<org_id>/users/mini-details", rank = 1)]
async fn get_org_user_mini_details(
org_id: OrganizationId,
_headers: ManagerHeadersLoose,
headers: ManagerHeadersLoose,
mut conn: DbConn,
) -> Json<Value> {
) -> JsonResult {
if org_id != headers.membership.org_uuid {
err!("Organization not found", "Organization id's do not match");
}
let mut members_json = Vec::new();
for m in Membership::find_by_org(&org_id, &mut conn).await {
members_json.push(m.to_json_mini_details(&mut conn).await);
}
Json(json!({
Ok(Json(json!({
"data": members_json,
"object": "list",
"continuationToken": null,
}))
})))
}
#[get("/organizations/<org_id>/users/<member_id>?<data..>", rank = 2)]
@ -1384,9 +1441,12 @@ async fn get_user(
org_id: OrganizationId,
member_id: MembershipId,
data: GetOrgUserData,
_headers: AdminHeaders,
headers: AdminHeaders,
mut conn: DbConn,
) -> JsonResult {
if org_id != headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
let Some(user) = Membership::find_by_uuid_and_org(&member_id, &org_id, &mut conn).await else {
err!("The specified user isn't a member of the organization")
};
@ -1430,6 +1490,9 @@ async fn edit_member(
headers: AdminHeaders,
mut conn: DbConn,
) -> EmptyResult {
if org_id != headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
let mut data: EditUserData = data.into_inner();
// HACK: We need the raw user-type to be sure custom role is selected to determine the access_all permission
@ -1551,7 +1614,10 @@ async fn bulk_delete_member(
headers: AdminHeaders,
mut conn: DbConn,
nt: Notify<'_>,
) -> Json<Value> {
) -> JsonResult {
if org_id != headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
let data: BulkMembershipIds = data.into_inner();
let mut bulk_response = Vec::new();
@ -1570,11 +1636,11 @@ async fn bulk_delete_member(
))
}
Json(json!({
Ok(Json(json!({
"data": bulk_response,
"object": "list",
"continuationToken": null
}))
})))
}
#[delete("/organizations/<org_id>/users/<member_id>")]
@ -1644,9 +1710,12 @@ async fn _delete_member(
async fn bulk_public_keys(
org_id: OrganizationId,
data: Json<BulkMembershipIds>,
_headers: AdminHeaders,
headers: AdminHeaders,
mut conn: DbConn,
) -> Json<Value> {
) -> JsonResult {
if org_id != headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
let data: BulkMembershipIds = data.into_inner();
let mut bulk_response = Vec::new();
@ -1670,11 +1739,11 @@ async fn bulk_public_keys(
}
}
Json(json!({
Ok(Json(json!({
"data": bulk_response,
"object": "list",
"continuationToken": null
}))
})))
}
use super::ciphers::update_cipher_from_data;
@ -1819,15 +1888,18 @@ async fn post_bulk_collections(data: Json<BulkCollectionsData>, headers: Headers
}
#[get("/organizations/<org_id>/policies")]
async fn list_policies(org_id: OrganizationId, _headers: AdminHeaders, mut conn: DbConn) -> Json<Value> {
async fn list_policies(org_id: OrganizationId, headers: AdminHeaders, mut conn: DbConn) -> JsonResult {
if org_id != headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
let policies = OrgPolicy::find_by_org(&org_id, &mut conn).await;
let policies_json: Vec<Value> = policies.iter().map(OrgPolicy::to_json).collect();
Json(json!({
Ok(Json(json!({
"data": policies_json,
"object": "list",
"continuationToken": null
}))
})))
}
#[get("/organizations/<org_id>/policies/token?<token>")]
@ -1855,7 +1927,10 @@ async fn list_policies_token(org_id: OrganizationId, token: &str, mut conn: DbCo
}
#[get("/organizations/<org_id>/policies/<pol_type>")]
async fn get_policy(org_id: OrganizationId, pol_type: i32, _headers: AdminHeaders, mut conn: DbConn) -> JsonResult {
async fn get_policy(org_id: OrganizationId, pol_type: i32, headers: AdminHeaders, mut conn: DbConn) -> JsonResult {
if org_id != headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
let Some(pol_type_enum) = OrgPolicyType::from_i32(pol_type) else {
err!("Invalid or unsupported policy type")
};
@ -1884,6 +1959,9 @@ async fn put_policy(
headers: AdminHeaders,
mut conn: DbConn,
) -> JsonResult {
if org_id != headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
let data: PolicyData = data.into_inner();
let Some(pol_type_enum) = OrgPolicyType::from_i32(pol_type) else {
@ -2217,7 +2295,7 @@ async fn bulk_deactivate_members(
data: Json<BulkRevokeMembershipIds>,
headers: AdminHeaders,
conn: DbConn,
) -> Json<Value> {
) -> JsonResult {
bulk_revoke_members(org_id, data, headers, conn).await
}
@ -2237,7 +2315,10 @@ async fn bulk_revoke_members(
data: Json<BulkRevokeMembershipIds>,
headers: AdminHeaders,
mut conn: DbConn,
) -> Json<Value> {
) -> JsonResult {
if org_id != headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
let data = data.into_inner();
let mut bulk_response = Vec::new();
@ -2261,11 +2342,11 @@ async fn bulk_revoke_members(
None => error!("No users to revoke"),
}
Json(json!({
Ok(Json(json!({
"data": bulk_response,
"object": "list",
"continuationToken": null
}))
})))
}
async fn _revoke_member(
@ -2326,7 +2407,7 @@ async fn bulk_activate_members(
data: Json<BulkMembershipIds>,
headers: AdminHeaders,
conn: DbConn,
) -> Json<Value> {
) -> JsonResult {
bulk_restore_members(org_id, data, headers, conn).await
}
@ -2346,7 +2427,10 @@ async fn bulk_restore_members(
data: Json<BulkMembershipIds>,
headers: AdminHeaders,
mut conn: DbConn,
) -> Json<Value> {
) -> JsonResult {
if org_id != headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
let data = data.into_inner();
let mut bulk_response = Vec::new();
@ -2365,11 +2449,11 @@ async fn bulk_restore_members(
));
}
Json(json!({
Ok(Json(json!({
"data": bulk_response,
"object": "list",
"continuationToken": null
}))
})))
}
async fn _restore_member(
@ -2426,7 +2510,10 @@ async fn _restore_member(
}
#[get("/organizations/<org_id>/groups")]
async fn get_groups(org_id: OrganizationId, _headers: ManagerHeadersLoose, mut conn: DbConn) -> JsonResult {
async fn get_groups(org_id: OrganizationId, headers: ManagerHeadersLoose, mut conn: DbConn) -> JsonResult {
if org_id != headers.membership.org_uuid {
err!("Organization not found", "Organization id's do not match");
}
let groups: Vec<Value> = if CONFIG.org_groups_enabled() {
// Group::find_by_organization(&org_id, &mut conn).await.iter().map(Group::to_json).collect::<Value>()
let groups = Group::find_by_organization(&org_id, &mut conn).await;
@ -2614,9 +2701,12 @@ async fn add_update_group(
async fn get_group_details(
org_id: OrganizationId,
group_id: GroupId,
_headers: AdminHeaders,
headers: AdminHeaders,
mut conn: DbConn,
) -> JsonResult {
if org_id != headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
if !CONFIG.org_groups_enabled() {
err!("Group support is disabled");
}
@ -2696,7 +2786,10 @@ async fn bulk_delete_groups(
}
#[get("/organizations/<org_id>/groups/<group_id>", rank = 2)]
async fn get_group(org_id: OrganizationId, group_id: GroupId, _headers: AdminHeaders, mut conn: DbConn) -> JsonResult {
async fn get_group(org_id: OrganizationId, group_id: GroupId, headers: AdminHeaders, mut conn: DbConn) -> JsonResult {
if org_id != headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
if !CONFIG.org_groups_enabled() {
err!("Group support is disabled");
}
@ -2712,9 +2805,12 @@ async fn get_group(org_id: OrganizationId, group_id: GroupId, _headers: AdminHea
async fn get_group_members(
org_id: OrganizationId,
group_id: GroupId,
_headers: AdminHeaders,
headers: AdminHeaders,
mut conn: DbConn,
) -> JsonResult {
if org_id != headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
if !CONFIG.org_groups_enabled() {
err!("Group support is disabled");
}
@ -2774,9 +2870,12 @@ async fn put_group_members(
async fn get_user_groups(
org_id: OrganizationId,
member_id: MembershipId,
_headers: AdminHeaders,
headers: AdminHeaders,
mut conn: DbConn,
) -> JsonResult {
if org_id != headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
if !CONFIG.org_groups_enabled() {
err!("Group support is disabled");
}
@ -2816,6 +2915,9 @@ async fn put_user_groups(
headers: AdminHeaders,
mut conn: DbConn,
) -> EmptyResult {
if org_id != headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
if !CONFIG.org_groups_enabled() {
err!("Group support is disabled");
}
@ -2865,6 +2967,9 @@ async fn delete_group_member(
headers: AdminHeaders,
mut conn: DbConn,
) -> EmptyResult {
if org_id != headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
if !CONFIG.org_groups_enabled() {
err!("Group support is disabled");
}
@ -2910,7 +3015,14 @@ struct OrganizationUserResetPasswordRequest {
// But the clients do not seem to use this at all
// Just add it here in case they will
#[get("/organizations/<org_id>/public-key")]
async fn get_organization_public_key(org_id: OrganizationId, _headers: Headers, mut conn: DbConn) -> JsonResult {
async fn get_organization_public_key(
org_id: OrganizationId,
headers: OrgMemberHeaders,
mut conn: DbConn,
) -> JsonResult {
if org_id != headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
let Some(org) = Organization::find_by_uuid(&org_id, &mut conn).await else {
err!("Organization not found")
};
@ -2924,7 +3036,7 @@ async fn get_organization_public_key(org_id: OrganizationId, _headers: Headers,
// Obsolete - Renamed to public-key (2023.8), left for backwards compatibility with older clients
// https://github.com/bitwarden/server/blob/25dc0c9178e3e3584074bbef0d4be827b7c89415/src/Api/AdminConsole/Controllers/OrganizationsController.cs#L463-L468
#[get("/organizations/<org_id>/keys")]
async fn get_organization_keys(org_id: OrganizationId, headers: Headers, conn: DbConn) -> JsonResult {
async fn get_organization_keys(org_id: OrganizationId, headers: OrgMemberHeaders, conn: DbConn) -> JsonResult {
get_organization_public_key(org_id, headers, conn).await
}
@ -2937,6 +3049,9 @@ async fn put_reset_password(
mut conn: DbConn,
nt: Notify<'_>,
) -> EmptyResult {
if org_id != headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
let Some(org) = Organization::find_by_uuid(&org_id, &mut conn).await else {
err!("Required organization not found")
};
@ -2993,6 +3108,9 @@ async fn get_reset_password_details(
headers: AdminHeaders,
mut conn: DbConn,
) -> JsonResult {
if org_id != headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
let Some(org) = Organization::find_by_uuid(&org_id, &mut conn).await else {
err!("Required organization not found")
};
@ -3114,7 +3232,10 @@ async fn get_org_export(
headers: AdminHeaders,
client_version: Option<ClientVersion>,
mut conn: DbConn,
) -> Json<Value> {
) -> JsonResult {
if org_id != headers.org_id {
err!("Organization not found", "Organization id's do not match");
}
// Since version v2023.1.0 the format of the export is different.
// Also, this endpoint was created since v2022.9.0.
// Therefore, we will check for any version smaller then v2023.1.0 and return a different response.
@ -3130,7 +3251,7 @@ async fn get_org_export(
// Also both main keys here need to be lowercase, else the export will fail.
if use_list_response_model {
// Backwards compatible pre v2023.1.0 response
Json(json!({
Ok(Json(json!({
"collections": {
"data": convert_json_key_lcase_first(_get_org_collections(&org_id, &mut conn).await),
"object": "list",
@ -3141,13 +3262,13 @@ async fn get_org_export(
"object": "list",
"continuationToken": null,
}
}))
})))
} else {
// v2023.1.0 and newer response
Json(json!({
Ok(Json(json!({
"collections": convert_json_key_lcase_first(_get_org_collections(&org_id, &mut conn).await),
"ciphers": convert_json_key_lcase_first(_get_org_details(&org_id, &headers.host, &headers.user.uuid, &mut conn).await),
}))
})))
}
}

View File

@ -557,24 +557,17 @@ impl<'r> FromRequest<'r> for OrgHeaders {
// but there are cases where it is a query value.
// First check the path, if this is not a valid uuid, try the query values.
let url_org_id: Option<OrganizationId> = {
let mut url_org_id = None;
if let Some(Ok(org_id)) = request.param::<&str>(1) {
if uuid::Uuid::parse_str(org_id).is_ok() {
url_org_id = Some(org_id.to_string().into());
}
if let Some(Ok(org_id)) = request.param::<OrganizationId>(1) {
Some(org_id.clone())
} else if let Some(Ok(org_id)) = request.query_value::<OrganizationId>("organizationId") {
Some(org_id.clone())
} else {
None
}
if let Some(Ok(org_id)) = request.query_value::<&str>("organizationId") {
if uuid::Uuid::parse_str(org_id).is_ok() {
url_org_id = Some(org_id.to_string().into());
}
}
url_org_id
};
match url_org_id {
Some(org_id) => {
Some(org_id) if uuid::Uuid::parse_str(&org_id).is_ok() => {
let mut conn = match DbConn::from_request(request).await {
Outcome::Success(conn) => conn,
_ => err_handler!("Error getting DB"),
@ -619,6 +612,7 @@ pub struct AdminHeaders {
pub user: User,
pub membership_type: MembershipType,
pub ip: ClientIp,
pub org_id: OrganizationId,
}
#[rocket::async_trait]
@ -634,6 +628,7 @@ impl<'r> FromRequest<'r> for AdminHeaders {
user: headers.user,
membership_type: headers.membership_type,
ip: headers.ip,
org_id: headers.membership.org_uuid,
})
} else {
err_handler!("You need to be Admin or Owner to call this endpoint")
@ -679,6 +674,7 @@ pub struct ManagerHeaders {
pub device: Device,
pub user: User,
pub ip: ClientIp,
pub org_id: OrganizationId,
}
#[rocket::async_trait]
@ -707,6 +703,7 @@ impl<'r> FromRequest<'r> for ManagerHeaders {
device: headers.device,
user: headers.user,
ip: headers.ip,
org_id: headers.membership.org_uuid,
})
} else {
err_handler!("You need to be a Manager, Admin or Owner to call this endpoint")
@ -786,6 +783,7 @@ impl ManagerHeaders {
device: h.device,
user: h.user,
ip: h.ip,
org_id: h.membership.org_uuid,
})
}
}
@ -794,6 +792,7 @@ pub struct OwnerHeaders {
pub device: Device,
pub user: User,
pub ip: ClientIp,
pub org_id: OrganizationId,
}
#[rocket::async_trait]
@ -807,6 +806,7 @@ impl<'r> FromRequest<'r> for OwnerHeaders {
device: headers.device,
user: headers.user,
ip: headers.ip,
org_id: headers.membership.org_uuid,
})
} else {
err_handler!("You need to be Owner to call this endpoint")
@ -814,6 +814,30 @@ impl<'r> FromRequest<'r> for OwnerHeaders {
}
}
pub struct OrgMemberHeaders {
pub host: String,
pub user: User,
pub org_id: OrganizationId,
}
#[rocket::async_trait]
impl<'r> FromRequest<'r> for OrgMemberHeaders {
type Error = &'static str;
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
let headers = try_outcome!(OrgHeaders::from_request(request).await);
if headers.membership_type >= MembershipType::User {
Outcome::Success(Self {
host: headers.host,
user: headers.user,
org_id: headers.membership.org_uuid,
})
} else {
err_handler!("You need to be a Member of the Organization to call this endpoint")
}
}
}
//
// Client IP address detection
//

View File

@ -1,8 +1,10 @@
use std::env::consts::EXE_SUFFIX;
use std::process::exit;
use std::sync::{
atomic::{AtomicBool, Ordering},
RwLock,
use std::{
env::consts::EXE_SUFFIX,
process::exit,
sync::{
atomic::{AtomicBool, Ordering},
RwLock,
},
};
use job_scheduler_ng::Schedule;
@ -12,7 +14,7 @@ use reqwest::Url;
use crate::{
db::DbConnType,
error::Error,
util::{get_env, get_env_bool, get_web_vault_version, parse_experimental_client_feature_flags},
util::{get_env, get_env_bool, get_web_vault_version, is_valid_email, parse_experimental_client_feature_flags},
};
static CONFIG_FILE: Lazy<String> = Lazy::new(|| {
@ -114,6 +116,14 @@ macro_rules! make_config {
serde_json::from_str(&config_str).map_err(Into::into)
}
fn clear_non_editable(&mut self) {
$($(
if !$editable {
self.$name = None;
}
)+)+
}
/// Merges the values of both builders into a new builder.
/// If both have the same element, `other` wins.
fn merge(&self, other: &Self, show_overrides: bool, overrides: &mut Vec<String>) -> Self {
@ -677,7 +687,7 @@ make_config! {
/// Use Sendmail |> Whether to send mail via the `sendmail` command
use_sendmail: bool, true, def, false;
/// Sendmail Command |> Which sendmail command to use. The one found in the $PATH is used if not specified.
sendmail_command: String, true, option;
sendmail_command: String, false, option;
/// Host
smtp_host: String, true, option;
/// DEPRECATED smtp_ssl |> DEPRECATED - Please use SMTP_SECURITY
@ -893,12 +903,12 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
let command = cfg.sendmail_command.clone().unwrap_or_else(|| format!("sendmail{EXE_SUFFIX}"));
let mut path = std::path::PathBuf::from(&command);
// Check if we can find the sendmail command to execute when no absolute path is given
if !path.is_absolute() {
match which::which(&command) {
Ok(result) => path = result,
Err(_) => err!(format!("sendmail command {command:?} not found in $PATH")),
}
let Ok(which_path) = which::which(&command) else {
err!(format!("sendmail command {command} not found in $PATH"))
};
path = which_path;
}
match path.metadata() {
@ -932,8 +942,8 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
}
}
if (cfg.smtp_host.is_some() || cfg.use_sendmail) && !cfg.smtp_from.contains('@') {
err!("SMTP_FROM does not contain a mandatory @ sign")
if !is_valid_email(&cfg.smtp_from) {
err!(format!("SMTP_FROM '{}' is not a valid email address", cfg.smtp_from))
}
if cfg._enable_email_2fa && cfg.email_token_size < 6 {
@ -1146,12 +1156,17 @@ impl Config {
})
}
pub fn update_config(&self, other: ConfigBuilder) -> Result<(), Error> {
pub fn update_config(&self, other: ConfigBuilder, ignore_non_editable: bool) -> Result<(), Error> {
// Remove default values
//let builder = other.remove(&self.inner.read().unwrap()._env);
// TODO: Remove values that are defaults, above only checks those set by env and not the defaults
let builder = other;
let mut builder = other;
// Remove values that are not editable
if ignore_non_editable {
builder.clear_non_editable();
}
// Serialize now before we consume the builder
let config_str = serde_json::to_string_pretty(&builder)?;
@ -1186,7 +1201,7 @@ impl Config {
let mut _overrides = Vec::new();
usr.merge(&other, false, &mut _overrides)
};
self.update_config(builder)
self.update_config(builder, false)
}
/// Tests whether an email's domain is allowed. A domain is allowed if it

View File

@ -152,6 +152,7 @@ impl PartialOrd<MembershipType> for i32 {
/// Local methods
impl Organization {
pub fn new(name: String, billing_email: String, private_key: Option<String>, public_key: Option<String>) -> Self {
let billing_email = billing_email.to_lowercase();
Self {
uuid: OrganizationId(crate::util::get_uuid()),
name,
@ -307,8 +308,8 @@ use crate::error::MapResult;
/// Database methods
impl Organization {
pub async fn save(&self, conn: &mut DbConn) -> EmptyResult {
if !email_address::EmailAddress::is_valid(self.billing_email.trim()) {
err!(format!("BillingEmail {} is not a valid email address", self.billing_email.trim()))
if !crate::util::is_valid_email(&self.billing_email) {
err!(format!("BillingEmail {} is not a valid email address", self.billing_email))
}
for member in Membership::find_by_org(&self.uuid, conn).await.iter() {

View File

@ -267,8 +267,8 @@ impl User {
}
pub async fn save(&mut self, conn: &mut DbConn) -> EmptyResult {
if self.email.trim().is_empty() {
err!("User email can't be empty")
if !crate::util::is_valid_email(&self.email) {
err!(format!("User email {} is not a valid email address", self.email))
}
self.updated_at = Utc::now().naive_utc();
@ -408,8 +408,8 @@ impl Invitation {
}
pub async fn save(&self, conn: &mut DbConn) -> EmptyResult {
if self.email.trim().is_empty() {
err!("Invitation email can't be empty")
if !crate::util::is_valid_email(&self.email) {
err!(format!("Invitation email {} is not a valid email address", self.email))
}
db_run! {conn:

View File

@ -1,7 +1,6 @@
use std::str::FromStr;
use chrono::NaiveDateTime;
use percent_encoding::{percent_encode, NON_ALPHANUMERIC};
use std::{env::consts::EXE_SUFFIX, str::FromStr};
use lettre::{
message::{Attachment, Body, Mailbox, Message, MultiPart, SinglePart},
@ -26,7 +25,7 @@ fn sendmail_transport() -> AsyncSendmailTransport<Tokio1Executor> {
if let Some(command) = CONFIG.sendmail_command() {
AsyncSendmailTransport::new_with_command(command)
} else {
AsyncSendmailTransport::new()
AsyncSendmailTransport::new_with_command(format!("sendmail{EXE_SUFFIX}"))
}
}
@ -595,13 +594,13 @@ async fn send_with_selected_transport(email: Message) -> EmptyResult {
// Match some common errors and make them more user friendly
Err(e) => {
if e.is_client() {
debug!("Sendmail client error: {:#?}", e);
debug!("Sendmail client error: {:?}", e);
err!(format!("Sendmail client error: {e}"));
} else if e.is_response() {
debug!("Sendmail response error: {:#?}", e);
debug!("Sendmail response error: {:?}", e);
err!(format!("Sendmail response error: {e}"));
} else {
debug!("Sendmail error: {:#?}", e);
debug!("Sendmail error: {:?}", e);
err!(format!("Sendmail error: {e}"));
}
}

View File

@ -236,8 +236,11 @@ function checkSecurityHeaders(headers, omit) {
"referrer-policy": ["same-origin"],
"x-xss-protection": ["0"],
"x-robots-tag": ["noindex", "nofollow"],
"cross-origin-resource-policy": ["same-origin"],
"content-security-policy": [
"default-src 'self'",
"default-src 'none'",
"font-src 'self'",
"manifest-src 'self'",
"base-uri 'self'",
"form-action 'self'",
"object-src 'self' blob:",

View File

@ -55,6 +55,8 @@ impl Fairing for AppHeaders {
res.set_raw_header("Referrer-Policy", "same-origin");
res.set_raw_header("X-Content-Type-Options", "nosniff");
res.set_raw_header("X-Robots-Tag", "noindex, nofollow");
res.set_raw_header("Cross-Origin-Resource-Policy", "same-origin");
// Obsolete in modern browsers, unsafe (XS-Leak), and largely replaced by CSP
res.set_raw_header("X-XSS-Protection", "0");
@ -74,7 +76,9 @@ impl Fairing for AppHeaders {
// # Mail Relay: https://bitwarden.com/blog/add-privacy-and-security-using-email-aliases-with-bitwarden/
// app.simplelogin.io, app.addy.io, api.fastmail.com, quack.duckduckgo.com
let csp = format!(
"default-src 'self'; \
"default-src 'none'; \
font-src 'self'; \
manifest-src 'self'; \
base-uri 'self'; \
form-action 'self'; \
object-src 'self' blob:; \
@ -461,6 +465,23 @@ pub fn parse_date(date: &str) -> NaiveDateTime {
DateTime::parse_from_rfc3339(date).unwrap().naive_utc()
}
/// Returns true or false if an email address is valid or not
///
/// Some extra checks instead of only using email_address
/// This prevents from weird email formats still excepted but in the end invalid
pub fn is_valid_email(email: &str) -> bool {
let Ok(email) = email_address::EmailAddress::from_str(email) else {
return false;
};
let Ok(email_url) = url::Url::parse(&format!("https://{}", email.domain())) else {
return false;
};
if email_url.path().ne("/") || email_url.domain().is_none() || email_url.query().is_some() {
return false;
}
true
}
//
// Deployment environment methods
//