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
- 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
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

View File

@@ -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:

View File

@@ -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<()>;

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(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);

View File

@@ -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!(

View File

@@ -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();

View File

@@ -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/

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
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:

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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);
}
}

View File

@@ -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();