Compare commits

..

13 Commits

Author SHA1 Message Date
realaravinth
5410a4657b feat: add changelog entry to doc change in access token verification
payload
2022-07-22 19:49:07 +05:30
realaravinth
7d0e4c6be4 fix: prevent sitekey abuse with account secret authentication for access token validation
SUMMARY
    At present, sitekey can be abused by installing it on a third-party
    site as verifying the access token returned from CAPTCHA validation
    doesn't require any authentication.

    This fix uses account secret authentication to verify access tokens

credits: by @gusted
2022-07-22 19:44:35 +05:30
realaravinth
85f91cb79b feat: update libmcaptcha 2022-07-21 18:29:16 +05:30
realaravinth
31978a83f2 fix: docker-compose -d up -> docker-compose up -d 2022-07-20 14:45:25 +05:30
realaravinth
22b312b8c5 fix: services.mcaptcha.depends_on must be a list 2022-07-20 14:44:46 +05:30
PierreC
2dce6eb2e8 Fixing some docs issues and adding some lines in docker-compose.yml (#33)
* Update README.md

* Update README.md

* Update DEPLOYMENT.md

* Update docker-compose.yml
2022-07-19 15:54:42 +05:30
realaravinth
c7d1bc3191 fix: rename postgres in docker-compose to avoid namespace collision
fixes: https://github.com/mCaptcha/mCaptcha/issues/31
2022-07-18 22:58:02 +05:30
Aravinth Manivannan
37004c7b4f Merge pull request #30 from felixonmars/patch-1
Correct typos in HACKING.md
2022-07-17 14:37:53 +05:30
Felix Yan
fa9a1a2f4c Correct typos in HACKING.md 2022-07-17 13:29:21 +08:00
realaravinth
5daeffd6fb chore: tests to verify mCaptcha counter 2022-05-31 12:46:09 +05:30
realaravinth
be9c6b757e fix: invert debug flag to set DB logging flag 2022-05-30 16:56:55 +05:30
realaravinth
b30bc67bd4 feat: conditional SQL statements logging for db-sqlx-postgres 2022-05-30 15:48:54 +05:30
realaravinth
a9f8cc24a6 feat: docker build caching with cargo-chef 2022-05-28 16:22:34 +05:30
14 changed files with 475 additions and 259 deletions

View File

@@ -2,4 +2,18 @@
### Changed ### 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

File diff suppressed because it is too large Load Diff

View File

@@ -17,23 +17,32 @@ COPY Makefile /src/
COPY scripts /src/scripts COPY scripts /src/scripts
RUN make frontend 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 FROM rust:latest as rust
WORKDIR /src WORKDIR /src
RUN mkdir src && echo "fn main() {}" > src/main.rs COPY . .
COPY Cargo.toml . COPY --from=cacher /src/target target
RUN sed -i '/.*build.rs.*/d' Cargo.toml #COPY --from=cacher /src/db/db-core/target /src/db/db-core/target
COPY Cargo.lock . #COPY --from=cacher /src/db/db-sqlx-postgres/target /src/db/db-sqlx-postgres/target
COPY migrations /src/migrations #COPY --from=cacher /src/db/db-migrations/target /src/db/db-migrations/target
COPY sqlx-data.json /src/ #COPY --from=cacher /src/utils/cache-bust/target /src/utils/cache-bust/target
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 --from=frontend /src/static/cache/bundle/ /src/static/cache/bundle/ COPY --from=frontend /src/static/cache/bundle/ /src/static/cache/bundle/
RUN cargo build --release 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 LABEL org.opencontainers.image.source https://github.com/mCaptcha/mCaptcha
RUN useradd -ms /bin/bash -u 1001 mcaptcha RUN useradd -ms /bin/bash -u 1001 mcaptcha
WORKDIR /home/mcaptcha WORKDIR /home/mcaptcha

View File

@@ -104,7 +104,7 @@ Clone the repo and run the following from the root of the repo:
```bash ```bash
git clone https://github.com/mCaptcha/mCaptcha.git 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: 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 :) 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. methods.
## Development: ## Development:
@@ -124,7 +124,7 @@ See [HACKING.md](./docs/HACKING.md)
## Deployment: ## Deployment:
See [DEPLOYMENT.md](./docs/DEPLOYMET.md) See [DEPLOYMENT.md](./docs/DEPLOYMENT.md)
## Configuration: ## Configuration:

View File

@@ -134,6 +134,9 @@ pub trait MCDatabase: std::marker::Send + std::marker::Sync + CloneSPDatabase {
/// get a user's secret /// get a user's secret
async fn get_secret(&self, username: &str) -> DBResult<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 /// update a user's secret
async fn update_secret(&self, username: &str, secret: &str) -> DBResult<()>; async fn update_secret(&self, username: &str, secret: &str) -> DBResult<()>;

View File

@@ -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(None, c.key).await.unwrap());
assert!(db.captcha_exists(Some(p.username), 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 // get captcha configuration
let captcha = db.get_captcha_config(p.username, c.key).await.unwrap(); let captcha = db.get_captcha_config(p.username, c.key).await.unwrap();
assert_eq!(captcha.key, c.key); assert_eq!(captcha.key, c.key);

View File

@@ -14,10 +14,13 @@
* You should have received a copy of the GNU Affero General Public License * 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/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
use std::str::FromStr;
use db_core::dev::*; use db_core::dev::*;
use sqlx::postgres::PgPoolOptions; use sqlx::postgres::PgPoolOptions;
use sqlx::types::time::OffsetDateTime; use sqlx::types::time::OffsetDateTime;
use sqlx::ConnectOptions;
use sqlx::PgPool; use sqlx::PgPool;
pub mod errors; pub mod errors;
@@ -42,6 +45,7 @@ pub enum ConnectionOptions {
pub struct Fresh { pub struct Fresh {
pub pool_options: PgPoolOptions, pub pool_options: PgPoolOptions,
pub disable_logging: bool,
pub url: String, pub url: String,
} }
@@ -63,11 +67,22 @@ impl Connect for ConnectionOptions {
type Pool = Database; type Pool = Database;
async fn connect(self) -> DBResult<Self::Pool> { async fn connect(self) -> DBResult<Self::Pool> {
let pool = match self { let pool = match self {
Self::Fresh(fresh) => fresh Self::Fresh(fresh) => {
.pool_options let mut connect_options =
.connect(&fresh.url) sqlx::postgres::PgConnectOptions::from_str(&fresh.url).unwrap();
.await if fresh.disable_logging {
.map_err(|e| DBError::DBError(Box::new(e)))?, connect_options.disable_statement_logging();
}
sqlx::postgres::PgConnectOptions::from_str(&fresh.url)
.unwrap()
.disable_statement_logging();
fresh
.pool_options
.connect_with(connect_options)
.await
.map_err(|e| DBError::DBError(Box::new(e)))?
}
Self::Existing(conn) => conn.0, Self::Existing(conn) => conn.0,
}; };
Ok(Database { pool }) Ok(Database { pool })
@@ -284,6 +299,22 @@ impl MCDatabase for Database {
Ok(secret) 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 /// update a user's secret
async fn update_secret(&self, username: &str, secret: &str) -> DBResult<()> { async fn update_secret(&self, username: &str, secret: &str) -> DBResult<()> {
sqlx::query!( sqlx::query!(

View File

@@ -68,7 +68,11 @@ async fn everyting_works() {
let url = env::var("POSTGRES_DATABASE_URL").unwrap(); let url = env::var("POSTGRES_DATABASE_URL").unwrap();
let pool_options = PgPoolOptions::new().max_connections(2); 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(); let db = connection_options.connect().await.unwrap();
db.migrate().await.unwrap(); db.migrate().await.unwrap();

View File

@@ -1,4 +1,4 @@
version: '3.9' version: "3.9"
services: services:
mcaptcha: mcaptcha:
@@ -6,11 +6,15 @@ services:
ports: ports:
- 7000:7000 - 7000:7000
environment: 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/ MCAPTCHA_REDIS_URL: redis://mcaptcha-redis/
RUST_LOG: debug RUST_LOG: debug
PORT: 7000
depends_on:
- mcaptcha-postgres
- mcaptcha-redis
postgres: mcaptcha_postgres:
image: postgres:13.2 image: postgres:13.2
volumes: volumes:
- mcaptcha-data:/var/lib/postgresql/ - mcaptcha-data:/var/lib/postgresql/

View File

@@ -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 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 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. a database instance mcaptcha instance.
## With docker-compose ## With docker-compose
1. Follow steps above to build docker image. 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: 3. Launch network:

View File

@@ -60,7 +60,7 @@ refer to [official instructions](https://www.gnu.org/software/make/)
### External Dependencies: ### External Dependencies:
### Postgres databse: ### Postgres database:
The backend requires a Postgres database. We have The backend requires a Postgres database. We have
compiletime SQL checks so without a database available, you won't be compiletime SQL checks so without a database available, you won't be
@@ -125,7 +125,7 @@ $ make
default Run app in debug mode default Run app in debug mode
clean Delete build artifacts clean Delete build artifacts
coverage Generate code coverage report in HTML format coverage Generate code coverage report in HTML format
dev-env Setup development environtment dev-env Setup development environment
doc Generate documentation doc Generate documentation
docker Build Docker image docker Build Docker image
docker-publish Build and publish Docker image docker-publish Build and publish Docker image

View File

@@ -98,7 +98,8 @@ pub async fn get_config(
/// ///
/// This fn gets mcaptcha config from database, builds [Defense][libmcaptcha::Defense], /// This fn gets mcaptcha config from database, builds [Defense][libmcaptcha::Defense],
/// creates [MCaptcha][libmcaptcha::MCaptcha] and adds it to [Master][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 // get levels
let levels = data.db.get_captcha_levels(None, key).await?; let levels = data.db.get_captcha_levels(None, key).await?;
let duration = data.db.get_captcha_cooldown(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()?; let defense = defense.build()?;
println!("{:?}", defense);
// create captcha // create captcha
let mcaptcha = MCaptchaBuilder::default() let mcaptcha = MCaptchaBuilder::default()
@@ -182,4 +184,98 @@ pub mod tests {
let config: PoWConfig = test::read_body_json(get_config_resp).await; let config: PoWConfig = test::read_body_json(get_config_resp).await;
assert_eq!(config.difficulty_factor, L1.difficulty_factor); 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
}
} }

View File

@@ -29,23 +29,41 @@ pub struct CaptchaValidateResp {
pub valid: bool, 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 // API keys are mcaptcha actor names
/// route hander that validates a PoW solution token /// route hander that validates a PoW solution token
#[my_codegen::post(path = "V1_API_ROUTES.pow.validate_captcha_token()")] #[my_codegen::post(path = "V1_API_ROUTES.pow.validate_captcha_token()")]
pub async fn validate_captcha_token( pub async fn validate_captcha_token(
payload: web::Json<VerifyCaptchaResult>, payload: web::Json<VerifyCaptchaResultPayload>,
data: AppData, data: AppData,
) -> ServiceResult<impl Responder> { ) -> 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 key = payload.key.clone();
let res = data let res = data.captcha.validate_verification_tokens(payload).await?;
.captcha let resp = CaptchaValidateResp { valid: res };
.validate_verification_tokens(payload.into_inner())
.await?;
let payload = CaptchaValidateResp { valid: res };
data.stats.record_confirm(&data, &key).await?; data.stats.record_confirm(&data, &key).await?;
//println!("{:?}", &payload); //println!("{:?}", &payload);
Ok(HttpResponse::Ok().json(payload)) Ok(HttpResponse::Ok().json(resp))
} }
#[cfg(test)] #[cfg(test)]
@@ -76,8 +94,21 @@ pub mod tests {
delete_user(data, NAME).await; delete_user(data, NAME).await;
register_and_signin(data, NAME, EMAIL, PASSWORD).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 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 { let get_config_payload = GetConfigPayload {
key: token_key.key.clone(), key: token_key.key.clone(),
@@ -116,11 +147,35 @@ pub mod tests {
assert_eq!(pow_verify_resp.status(), StatusCode::OK); assert_eq!(pow_verify_resp.status(), StatusCode::OK);
let client_token: ValidationToken = test::read_body_json(pow_verify_resp).await; 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(), token: client_token.token.clone(),
key: token_key.key.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( let validate_client_token = test::call_service(
&app, &app,
post_request!(&validate_payload, VERIFY_TOKEN_URL).to_request(), post_request!(&validate_payload, VERIFY_TOKEN_URL).to_request(),
@@ -139,19 +194,5 @@ pub mod tests {
.await; .await;
let resp: CaptchaValidateResp = test::read_body_json(string_not_found).await; let resp: CaptchaValidateResp = test::read_body_json(string_not_found).await;
assert!(!resp.valid); 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);
} }
} }

View File

@@ -190,6 +190,7 @@ impl Data {
let connection_options = ConnectionOptions::Fresh(Fresh { let connection_options = ConnectionOptions::Fresh(Fresh {
pool_options, pool_options,
url: s.database.url.clone(), url: s.database.url.clone(),
disable_logging: !s.debug,
}); });
let db = connection_options.connect().await.unwrap(); let db = connection_options.connect().await.unwrap();
db.migrate().await.unwrap(); db.migrate().await.unwrap();