cc-d / flask-simple-captcha

lightweight captcha. doesn't require server side sessions packages
MIT License
30 stars 12 forks source link

Easily Bypassed #9

Closed racinette closed 1 year ago

racinette commented 2 years ago

I've been too thinking about such sweet-sweet captcha implementation. Here is my post on stackoverflow: https://stackoverflow.com/questions/69357107/captcha-result-persistence-w-o-server-side-sessions

Long story short: should the attacker solve any captcha once, they can send the same input + hash all the time, without limits, since they are correct, yet your server has no idea they have expired / been used.

I've though of salting the hash, but it doesn't matter, since you should be sending the salt to the form too. There is also a possibility of keeping the salt server-side and changing it every 15 minutes, but it's pointless too.

I haven't figured out a way to make it happen. There seems to be no way, unfortunately, except for persisting captchas on the server in a session (server-side) or in a database. I'm sure there should be a cryptographic solution to the problem (people invented decentralized money, client-side captcha shouldn't be a problem).

capton586 commented 1 year ago

Try to use TimedJSONWebSignatureSerializer from itsdangerous. Like this:

from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
s = Serializer('secret_key', 60)
id = s.loads({'captcha-hash': captcha-hash})

So you can use the id to instead captcha-hash, then:

try:
    captcha-hash = s.dumps(id)['captcha-hash']

if the token is valid go to captcha verify, else return the error and create the new captcha. Unfortunately, this solution can't block the attack within 60s.

cc-d commented 1 year ago

hi. this was kind of an abandoned project for a while. i am far stronger of a coderguy today than i was 6 years ago, i'm considering prioritizing this because it's somewhat interesting and kind of my responsibility i guess

cc-d commented 1 year ago

i'm going to work on this today, as well as writing tests etc. it seems like an interesting problem.

cc-d commented 1 year ago

so as of today, i have fixed the code in the repo to solve these issues, and will be updating the package in pypy once i have tests written to confirm that nothing is broke. i ended up solving this by having an additional random salt created on init, giving each password hash a uuid and timestamp, with that uuid being submitted as a hidden form. ill finish this up and upload the package prob tonight

cc-d commented 1 year ago

fixed as of tonight and version 2.0

cc-d commented 1 year ago

for version 3.0 i'll go back to a model without any server side state tracking happening i'll use jwts or similar

cc-d commented 1 year ago

version 3.0 published 8 hours ago, is 100% clientside and uses jwts should back support 1.0 versions too

cc-d commented 1 year ago

as soon as i get some time i'll ensure there exists regression tests for exactly this in 3.0, that this issue does not exist in 3.0, etc. version 2.0 fixed this kind of cheating using uuids and shared state of an object in memory, version 3 is using jwts but the resubmission bug might have been reintroduced from 2. again, regression tests are needed. if the resubmission bug is present then i'll have to get clever. in 2.0 there was shared memory state of a captcha class which kept track of this, but that does kind of completely defeat the whole purpose of not keeping tracks of things serverside. i don't want to rely on the shared memory state model if at all possible. there might be something useful possible here with jwts, the current model resubmission could be a potential issue without an absurdly short timeout

cc-d commented 1 year ago

okay so the only two versions are going to be v1 and v4

v4 uses jwts, uses a set() on the man CAPTCHA() object to prevent duplicate jwt submissions, and jwts expire after $x minutes. i ensured this was backwards compatible with 1.0, even if hash/text arg order is reversed.

not super happy w the solution but at least it's actually functional now and not so easily bypassed. i'm going to remove all versions other than v1/v4