mirror of
https://github.com/mCaptcha/mCaptcha.git
synced 2026-02-11 10:05:41 +00:00
Compare commits
13 Commits
db-abstrac
...
fix-siteke
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5410a4657b | ||
|
|
7d0e4c6be4 | ||
|
|
85f91cb79b | ||
|
|
31978a83f2 | ||
|
|
22b312b8c5 | ||
|
|
2dce6eb2e8 | ||
|
|
c7d1bc3191 | ||
|
|
37004c7b4f | ||
|
|
fa9a1a2f4c | ||
|
|
5daeffd6fb | ||
|
|
be9c6b757e | ||
|
|
b30bc67bd4 | ||
|
|
a9f8cc24a6 |
16
CHANGELOG.md
16
CHANGELOG.md
@@ -2,4 +2,18 @@
|
||||
|
||||
### Changed
|
||||
|
||||
- Rename pow section in settings to captcha and add options to configure([`42544ec42`](https://github.com/mCaptcha/mCaptcha/commit/42544ec421e0c3ec4a8d132e6101ab4069bf0065))
|
||||
- ([`7d0e4c6`](https://github.com/mCaptcha/mCaptcha/commit/7d0e4c6be4b0769921cda7681858ebe16ec9a07b)) Add `secret` parameter to token verification request payload(`/api/v1/pow/siteverify`) to mitigate a security issue that @gusted found:
|
||||
> ...A malicious user could grab the sitekey
|
||||
> and use that sitekey with mcaptcha to use it for their own server.
|
||||
> While they can now go abuse it for illegal stuff or other stuff.
|
||||
> You might decide, oh I don't want this! and terminate a legitimate
|
||||
> siteKey.
|
||||
> New request payload:
|
||||
```json
|
||||
{
|
||||
"secret": "<your-users-secret>", // found in /settings in the dashbaord
|
||||
"token": "<token-presented-by-the-user>",
|
||||
"key": "<your-sitekey>"
|
||||
}
|
||||
```
|
||||
- ([`42544ec42`](https://github.com/mCaptcha/mCaptcha/commit/42544ec421e0c3ec4a8d132e6101ab4069bf0065)) Rename pow section in settings to captcha and add options to configure
|
||||
|
||||
419
Cargo.lock
generated
419
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
33
Dockerfile
33
Dockerfile
@@ -17,23 +17,32 @@ COPY Makefile /src/
|
||||
COPY scripts /src/scripts
|
||||
RUN make frontend
|
||||
|
||||
FROM rust:latest as planner
|
||||
RUN cargo install cargo-chef
|
||||
WORKDIR /src
|
||||
COPY . /src/
|
||||
RUN cargo chef prepare --recipe-path recipe.json
|
||||
|
||||
FROM rust:latest as cacher
|
||||
WORKDIR /src/
|
||||
RUN cargo install cargo-chef
|
||||
COPY --from=planner /src/recipe.json recipe.json
|
||||
RUN cargo chef cook --release --recipe-path recipe.json
|
||||
|
||||
FROM rust:latest as rust
|
||||
WORKDIR /src
|
||||
RUN mkdir src && echo "fn main() {}" > src/main.rs
|
||||
COPY Cargo.toml .
|
||||
RUN sed -i '/.*build.rs.*/d' Cargo.toml
|
||||
COPY Cargo.lock .
|
||||
COPY migrations /src/migrations
|
||||
COPY sqlx-data.json /src/
|
||||
COPY src/tests-migrate.rs /src/src/tests-migrate.rs
|
||||
COPY src/settings.rs /src/src/settings.rs
|
||||
RUN cargo --version
|
||||
RUN cargo build --release
|
||||
COPY . /src
|
||||
COPY . .
|
||||
COPY --from=cacher /src/target target
|
||||
#COPY --from=cacher /src/db/db-core/target /src/db/db-core/target
|
||||
#COPY --from=cacher /src/db/db-sqlx-postgres/target /src/db/db-sqlx-postgres/target
|
||||
#COPY --from=cacher /src/db/db-migrations/target /src/db/db-migrations/target
|
||||
#COPY --from=cacher /src/utils/cache-bust/target /src/utils/cache-bust/target
|
||||
COPY --from=frontend /src/static/cache/bundle/ /src/static/cache/bundle/
|
||||
RUN cargo --version
|
||||
RUN make cache-bust
|
||||
RUN cargo build --release
|
||||
|
||||
FROM debian:bullseye
|
||||
FROM debian:bullseye as mCaptcha
|
||||
LABEL org.opencontainers.image.source https://github.com/mCaptcha/mCaptcha
|
||||
RUN useradd -ms /bin/bash -u 1001 mcaptcha
|
||||
WORKDIR /home/mcaptcha
|
||||
|
||||
@@ -104,7 +104,7 @@ Clone the repo and run the following from the root of the repo:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/mCaptcha/mCaptcha.git
|
||||
docker-compose -d up
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
After the containers are up, visit [http://localhost:7000](http://localhost:7000) and login with the default credentials:
|
||||
@@ -115,7 +115,7 @@ After the containers are up, visit [http://localhost:7000](http://localhost:7000
|
||||
|
||||
It takes a while to build the image so please be patient :)
|
||||
|
||||
See [DEPLOYMENT.md](./docs/DEPLOYMET.md) detailed alternate deployment
|
||||
See [DEPLOYMENT.md](./docs/DEPLOYMENT.md) detailed alternate deployment
|
||||
methods.
|
||||
|
||||
## Development:
|
||||
@@ -124,7 +124,7 @@ See [HACKING.md](./docs/HACKING.md)
|
||||
|
||||
## Deployment:
|
||||
|
||||
See [DEPLOYMENT.md](./docs/DEPLOYMET.md)
|
||||
See [DEPLOYMENT.md](./docs/DEPLOYMENT.md)
|
||||
|
||||
## Configuration:
|
||||
|
||||
|
||||
@@ -134,6 +134,9 @@ pub trait MCDatabase: std::marker::Send + std::marker::Sync + CloneSPDatabase {
|
||||
/// get a user's secret
|
||||
async fn get_secret(&self, username: &str) -> DBResult<Secret>;
|
||||
|
||||
/// get a user's secret from a captcha key
|
||||
async fn get_secret_from_captcha(&self, key: &str) -> DBResult<Secret>;
|
||||
|
||||
/// update a user's secret
|
||||
async fn update_secret(&self, username: &str, secret: &str) -> DBResult<()>;
|
||||
|
||||
|
||||
@@ -176,6 +176,10 @@ pub async fn database_works<'a, T: MCDatabase>(
|
||||
assert!(db.captcha_exists(None, c.key).await.unwrap());
|
||||
assert!(db.captcha_exists(Some(p.username), c.key).await.unwrap());
|
||||
|
||||
// get secret from captcha key
|
||||
let secret_from_captcha = db.get_secret_from_captcha(&c.key).await.unwrap();
|
||||
assert_eq!(secret_from_captcha.secret, p.secret, "user secret matches");
|
||||
|
||||
// get captcha configuration
|
||||
let captcha = db.get_captcha_config(p.username, c.key).await.unwrap();
|
||||
assert_eq!(captcha.key, c.key);
|
||||
|
||||
@@ -14,10 +14,13 @@
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
use std::str::FromStr;
|
||||
|
||||
use db_core::dev::*;
|
||||
|
||||
use sqlx::postgres::PgPoolOptions;
|
||||
use sqlx::types::time::OffsetDateTime;
|
||||
use sqlx::ConnectOptions;
|
||||
use sqlx::PgPool;
|
||||
|
||||
pub mod errors;
|
||||
@@ -42,6 +45,7 @@ pub enum ConnectionOptions {
|
||||
|
||||
pub struct Fresh {
|
||||
pub pool_options: PgPoolOptions,
|
||||
pub disable_logging: bool,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
@@ -63,11 +67,22 @@ impl Connect for ConnectionOptions {
|
||||
type Pool = Database;
|
||||
async fn connect(self) -> DBResult<Self::Pool> {
|
||||
let pool = match self {
|
||||
Self::Fresh(fresh) => fresh
|
||||
Self::Fresh(fresh) => {
|
||||
let mut connect_options =
|
||||
sqlx::postgres::PgConnectOptions::from_str(&fresh.url).unwrap();
|
||||
if fresh.disable_logging {
|
||||
connect_options.disable_statement_logging();
|
||||
}
|
||||
sqlx::postgres::PgConnectOptions::from_str(&fresh.url)
|
||||
.unwrap()
|
||||
.disable_statement_logging();
|
||||
fresh
|
||||
.pool_options
|
||||
.connect(&fresh.url)
|
||||
.connect_with(connect_options)
|
||||
.await
|
||||
.map_err(|e| DBError::DBError(Box::new(e)))?,
|
||||
.map_err(|e| DBError::DBError(Box::new(e)))?
|
||||
}
|
||||
|
||||
Self::Existing(conn) => conn.0,
|
||||
};
|
||||
Ok(Database { pool })
|
||||
@@ -284,6 +299,22 @@ impl MCDatabase for Database {
|
||||
Ok(secret)
|
||||
}
|
||||
|
||||
/// get a user's secret from a captcha key
|
||||
async fn get_secret_from_captcha(&self, key: &str) -> DBResult<Secret> {
|
||||
let secret = sqlx::query_as!(
|
||||
Secret,
|
||||
r#"SELECT secret FROM mcaptcha_users WHERE ID = (
|
||||
SELECT user_id FROM mcaptcha_config WHERE key = $1
|
||||
)"#,
|
||||
key,
|
||||
)
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.map_err(|e| map_row_not_found_err(e, DBError::AccountNotFound))?;
|
||||
|
||||
Ok(secret)
|
||||
}
|
||||
|
||||
/// update a user's secret
|
||||
async fn update_secret(&self, username: &str, secret: &str) -> DBResult<()> {
|
||||
sqlx::query!(
|
||||
|
||||
@@ -68,7 +68,11 @@ async fn everyting_works() {
|
||||
|
||||
let url = env::var("POSTGRES_DATABASE_URL").unwrap();
|
||||
let pool_options = PgPoolOptions::new().max_connections(2);
|
||||
let connection_options = ConnectionOptions::Fresh(Fresh { pool_options, url });
|
||||
let connection_options = ConnectionOptions::Fresh(Fresh {
|
||||
pool_options,
|
||||
url,
|
||||
disable_logging: false,
|
||||
});
|
||||
let db = connection_options.connect().await.unwrap();
|
||||
|
||||
db.migrate().await.unwrap();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
version: '3.9'
|
||||
version: "3.9"
|
||||
|
||||
services:
|
||||
mcaptcha:
|
||||
@@ -6,11 +6,15 @@ services:
|
||||
ports:
|
||||
- 7000:7000
|
||||
environment:
|
||||
DATABASE_URL: postgres://postgres:password@postgres:5432/postgres # set password at placeholder
|
||||
DATABASE_URL: postgres://postgres:password@mcaptcha_postgres:5432/postgres # set password at placeholder
|
||||
MCAPTCHA_REDIS_URL: redis://mcaptcha-redis/
|
||||
RUST_LOG: debug
|
||||
PORT: 7000
|
||||
depends_on:
|
||||
- mcaptcha-postgres
|
||||
- mcaptcha-redis
|
||||
|
||||
postgres:
|
||||
mcaptcha_postgres:
|
||||
image: postgres:13.2
|
||||
volumes:
|
||||
- mcaptcha-data:/var/lib/postgresql/
|
||||
|
||||
@@ -34,14 +34,14 @@ docker run -p <host-machine-port>:<port-in-configuration-file> \
|
||||
|
||||
If you don't have a Postgres instance running, you can either install
|
||||
one using a package manager or launch one with docker. A [docker-compose
|
||||
configuration]('../docker-compose.yml) is available that will launch both
|
||||
configuration](../docker-compose.yml) is available that will launch both
|
||||
a database instance mcaptcha instance.
|
||||
|
||||
## With docker-compose
|
||||
|
||||
1. Follow steps above to build docker image.
|
||||
|
||||
2. Set database password [docker-compose configuration]('../docker-compose.yml).
|
||||
2. Set database password [docker-compose configuration](../docker-compose.yml).
|
||||
|
||||
3. Launch network:
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ refer to [official instructions](https://www.gnu.org/software/make/)
|
||||
|
||||
### External Dependencies:
|
||||
|
||||
### Postgres databse:
|
||||
### Postgres database:
|
||||
|
||||
The backend requires a Postgres database. We have
|
||||
compiletime SQL checks so without a database available, you won't be
|
||||
@@ -125,7 +125,7 @@ $ make
|
||||
default Run app in debug mode
|
||||
clean Delete build artifacts
|
||||
coverage Generate code coverage report in HTML format
|
||||
dev-env Setup development environtment
|
||||
dev-env Setup development environment
|
||||
doc Generate documentation
|
||||
docker Build Docker image
|
||||
docker-publish Build and publish Docker image
|
||||
|
||||
@@ -98,7 +98,8 @@ pub async fn get_config(
|
||||
///
|
||||
/// This fn gets mcaptcha config from database, builds [Defense][libmcaptcha::Defense],
|
||||
/// creates [MCaptcha][libmcaptcha::MCaptcha] and adds it to [Master][libmcaptcha::Defense]
|
||||
async fn init_mcaptcha(data: &AppData, key: &str) -> ServiceResult<()> {
|
||||
pub async fn init_mcaptcha(data: &AppData, key: &str) -> ServiceResult<()> {
|
||||
println!("Initializing captcha");
|
||||
// get levels
|
||||
let levels = data.db.get_captcha_levels(None, key).await?;
|
||||
let duration = data.db.get_captcha_cooldown(key).await?;
|
||||
@@ -117,6 +118,7 @@ async fn init_mcaptcha(data: &AppData, key: &str) -> ServiceResult<()> {
|
||||
}
|
||||
|
||||
let defense = defense.build()?;
|
||||
println!("{:?}", defense);
|
||||
|
||||
// create captcha
|
||||
let mcaptcha = MCaptchaBuilder::default()
|
||||
@@ -182,4 +184,98 @@ pub mod tests {
|
||||
let config: PoWConfig = test::read_body_json(get_config_resp).await;
|
||||
assert_eq!(config.difficulty_factor, L1.difficulty_factor);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
pub async fn pow_difficulty_factor_increases_on_visitor_count_increase() {
|
||||
use super::*;
|
||||
use crate::tests::*;
|
||||
use crate::*;
|
||||
use actix_web::test;
|
||||
|
||||
use libmcaptcha::defense::Level;
|
||||
|
||||
use crate::api::v1::mcaptcha::create::CreateCaptcha;
|
||||
use crate::api::v1::mcaptcha::create::MCaptchaDetails;
|
||||
|
||||
const NAME: &str = "powusrworks2";
|
||||
const PASSWORD: &str = "testingpas";
|
||||
const EMAIL: &str = "randomuser2@a.com";
|
||||
pub const L1: Level = Level {
|
||||
difficulty_factor: 10,
|
||||
visitor_threshold: 10,
|
||||
};
|
||||
pub const L2: Level = Level {
|
||||
difficulty_factor: 20,
|
||||
visitor_threshold: 20,
|
||||
};
|
||||
|
||||
pub const L3: Level = Level {
|
||||
difficulty_factor: 30,
|
||||
visitor_threshold: 30,
|
||||
};
|
||||
|
||||
let data = get_data().await;
|
||||
let data = &data;
|
||||
let levels = [L1, L2, L3];
|
||||
|
||||
delete_user(data, NAME).await;
|
||||
|
||||
let (_, signin_resp) = register_and_signin(data, NAME, EMAIL, PASSWORD).await;
|
||||
let cookies = get_cookie!(signin_resp);
|
||||
let app = get_app!(data).await;
|
||||
|
||||
let create_captcha = CreateCaptcha {
|
||||
levels: levels.into(),
|
||||
duration: 30,
|
||||
description: "dummy".into(),
|
||||
};
|
||||
|
||||
// 1. add level
|
||||
let add_token_resp = test::call_service(
|
||||
&app,
|
||||
post_request!(&create_captcha, V1_API_ROUTES.captcha.create)
|
||||
.cookie(cookies.clone())
|
||||
.to_request(),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(add_token_resp.status(), StatusCode::OK);
|
||||
let token_key: MCaptchaDetails = test::read_body_json(add_token_resp).await;
|
||||
|
||||
let get_config_payload = GetConfigPayload {
|
||||
key: token_key.key.clone(),
|
||||
};
|
||||
|
||||
let url = V1_API_ROUTES.pow.get_config;
|
||||
let mut prev = 0;
|
||||
for (count, l) in levels.iter().enumerate() {
|
||||
for l in prev..l.visitor_threshold * 2 {
|
||||
let get_config_resp = test::call_service(
|
||||
&app,
|
||||
post_request!(&get_config_payload, V1_API_ROUTES.pow.get_config)
|
||||
.to_request(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
let get_config_resp = test::call_service(
|
||||
&app,
|
||||
post_request!(&get_config_payload, V1_API_ROUTES.pow.get_config)
|
||||
.to_request(),
|
||||
)
|
||||
.await;
|
||||
|
||||
let config: PoWConfig = test::read_body_json(get_config_resp).await;
|
||||
println!(
|
||||
"[{count}] received difficulty_factor: {} prev difficulty_factor {}",
|
||||
config.difficulty_factor, prev
|
||||
);
|
||||
if count == levels.len() - 1 {
|
||||
assert!(config.difficulty_factor == prev);
|
||||
} else {
|
||||
assert!(config.difficulty_factor > prev);
|
||||
}
|
||||
prev = config.difficulty_factor;
|
||||
}
|
||||
// update and check changes
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,23 +29,41 @@ pub struct CaptchaValidateResp {
|
||||
pub valid: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct VerifyCaptchaResultPayload {
|
||||
pub secret: String,
|
||||
pub key: String,
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
impl From<VerifyCaptchaResultPayload> for VerifyCaptchaResult {
|
||||
fn from(m: VerifyCaptchaResultPayload) -> Self {
|
||||
VerifyCaptchaResult {
|
||||
token: m.token,
|
||||
key: m.key,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// API keys are mcaptcha actor names
|
||||
|
||||
/// route hander that validates a PoW solution token
|
||||
#[my_codegen::post(path = "V1_API_ROUTES.pow.validate_captcha_token()")]
|
||||
pub async fn validate_captcha_token(
|
||||
payload: web::Json<VerifyCaptchaResult>,
|
||||
payload: web::Json<VerifyCaptchaResultPayload>,
|
||||
data: AppData,
|
||||
) -> ServiceResult<impl Responder> {
|
||||
let secret = data.db.get_secret_from_captcha(&payload.key).await?;
|
||||
if secret.secret != payload.secret {
|
||||
return Err(ServiceError::WrongPassword);
|
||||
}
|
||||
let payload: VerifyCaptchaResult = payload.into_inner().into();
|
||||
let key = payload.key.clone();
|
||||
let res = data
|
||||
.captcha
|
||||
.validate_verification_tokens(payload.into_inner())
|
||||
.await?;
|
||||
let payload = CaptchaValidateResp { valid: res };
|
||||
let res = data.captcha.validate_verification_tokens(payload).await?;
|
||||
let resp = CaptchaValidateResp { valid: res };
|
||||
data.stats.record_confirm(&data, &key).await?;
|
||||
//println!("{:?}", &payload);
|
||||
Ok(HttpResponse::Ok().json(payload))
|
||||
Ok(HttpResponse::Ok().json(resp))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -76,8 +94,21 @@ pub mod tests {
|
||||
delete_user(data, NAME).await;
|
||||
|
||||
register_and_signin(data, NAME, EMAIL, PASSWORD).await;
|
||||
let (_, _signin_resp, token_key) = add_levels_util(data, NAME, PASSWORD).await;
|
||||
let (_, signin_resp, token_key) = add_levels_util(data, NAME, PASSWORD).await;
|
||||
let app = get_app!(data).await;
|
||||
let cookies = get_cookie!(signin_resp);
|
||||
|
||||
let secret = test::call_service(
|
||||
&app,
|
||||
test::TestRequest::get()
|
||||
.cookie(cookies.clone())
|
||||
.uri(V1_API_ROUTES.account.get_secret)
|
||||
.to_request(),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(secret.status(), StatusCode::OK);
|
||||
let secret: db_core::Secret = test::read_body_json(secret).await;
|
||||
let secret = secret.secret;
|
||||
|
||||
let get_config_payload = GetConfigPayload {
|
||||
key: token_key.key.clone(),
|
||||
@@ -116,11 +147,35 @@ pub mod tests {
|
||||
assert_eq!(pow_verify_resp.status(), StatusCode::OK);
|
||||
let client_token: ValidationToken = test::read_body_json(pow_verify_resp).await;
|
||||
|
||||
let validate_payload = VerifyCaptchaResult {
|
||||
let mut validate_payload = VerifyCaptchaResultPayload {
|
||||
token: client_token.token.clone(),
|
||||
key: token_key.key.clone(),
|
||||
secret: NAME.to_string(),
|
||||
};
|
||||
|
||||
// siteverify authentication failure
|
||||
bad_post_req_test(
|
||||
data,
|
||||
NAME,
|
||||
PASSWORD,
|
||||
VERIFY_TOKEN_URL,
|
||||
&validate_payload,
|
||||
ServiceError::WrongPassword,
|
||||
)
|
||||
.await;
|
||||
// let validate_client_token = test::call_service(
|
||||
// &app,
|
||||
// post_request!(&validate_payload, VERIFY_TOKEN_URL).to_request(),
|
||||
// )
|
||||
// .await;
|
||||
// assert_eq!(validate_client_token.status(), StatusCode::OK);
|
||||
// let resp: CaptchaValidateResp =
|
||||
// test::read_body_json(validate_client_token).await;
|
||||
// assert!(resp.valid);
|
||||
|
||||
// verifying work
|
||||
validate_payload.secret = secret.clone();
|
||||
|
||||
let validate_client_token = test::call_service(
|
||||
&app,
|
||||
post_request!(&validate_payload, VERIFY_TOKEN_URL).to_request(),
|
||||
@@ -139,19 +194,5 @@ pub mod tests {
|
||||
.await;
|
||||
let resp: CaptchaValidateResp = test::read_body_json(string_not_found).await;
|
||||
assert!(!resp.valid);
|
||||
|
||||
let validate_payload = VerifyCaptchaResult {
|
||||
token: client_token.token.clone(),
|
||||
key: client_token.token.clone(),
|
||||
};
|
||||
|
||||
// key not found
|
||||
let key_not_found = test::call_service(
|
||||
&app,
|
||||
post_request!(&validate_payload, VERIFY_TOKEN_URL).to_request(),
|
||||
)
|
||||
.await;
|
||||
let resp: CaptchaValidateResp = test::read_body_json(key_not_found).await;
|
||||
assert!(!resp.valid);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,6 +190,7 @@ impl Data {
|
||||
let connection_options = ConnectionOptions::Fresh(Fresh {
|
||||
pool_options,
|
||||
url: s.database.url.clone(),
|
||||
disable_logging: !s.debug,
|
||||
});
|
||||
let db = connection_options.connect().await.unwrap();
|
||||
db.migrate().await.unwrap();
|
||||
|
||||
Reference in New Issue
Block a user