mirror of
https://github.com/iv-org/invidious
synced 2024-11-27 21:43:28 +01:00
Remember nonce to prevent replay attacks
This commit is contained in:
parent
c7f0a6f2e1
commit
d185ba84bf
13
config/sql/nonces.sql
Normal file
13
config/sql/nonces.sql
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
-- Table: public.nonces
|
||||||
|
|
||||||
|
-- DROP TABLE public.nonces;
|
||||||
|
|
||||||
|
CREATE TABLE public.nonces
|
||||||
|
(
|
||||||
|
nonce text
|
||||||
|
)
|
||||||
|
WITH (
|
||||||
|
OIDS=FALSE
|
||||||
|
);
|
||||||
|
|
||||||
|
GRANT ALL ON TABLE public.nonces TO kemal;
|
1
setup.sh
1
setup.sh
@ -7,3 +7,4 @@ psql invidious < config/sql/channels.sql
|
|||||||
psql invidious < config/sql/videos.sql
|
psql invidious < config/sql/videos.sql
|
||||||
psql invidious < config/sql/channel_videos.sql
|
psql invidious < config/sql/channel_videos.sql
|
||||||
psql invidious < config/sql/users.sql
|
psql invidious < config/sql/users.sql
|
||||||
|
psql invidious < config/sql/nonces.sql
|
||||||
|
@ -142,7 +142,7 @@ before_all do |env|
|
|||||||
user = PG_DB.query_one?("SELECT * FROM users WHERE $1 = ANY(id)", sid, as: User)
|
user = PG_DB.query_one?("SELECT * FROM users WHERE $1 = ANY(id)", sid, as: User)
|
||||||
|
|
||||||
if user
|
if user
|
||||||
challenge, token = create_response(user.email, "sign_out", HMAC_KEY, 1.week)
|
challenge, token = create_response(user.email, "sign_out", HMAC_KEY, PG_DB, 1.week)
|
||||||
|
|
||||||
env.set "challenge", challenge
|
env.set "challenge", challenge
|
||||||
env.set "token", token
|
env.set "token", token
|
||||||
@ -155,7 +155,7 @@ before_all do |env|
|
|||||||
client = make_client(YT_URL)
|
client = make_client(YT_URL)
|
||||||
user = get_user(sid, client, headers, PG_DB, false)
|
user = get_user(sid, client, headers, PG_DB, false)
|
||||||
|
|
||||||
challenge, token = create_response(user.email, "sign_out", HMAC_KEY, 1.week)
|
challenge, token = create_response(user.email, "sign_out", HMAC_KEY, PG_DB, 1.week)
|
||||||
env.set "challenge", challenge
|
env.set "challenge", challenge
|
||||||
env.set "token", token
|
env.set "token", token
|
||||||
|
|
||||||
@ -624,7 +624,7 @@ get "/login" do |env|
|
|||||||
account_type ||= "invidious"
|
account_type ||= "invidious"
|
||||||
|
|
||||||
if account_type == "invidious"
|
if account_type == "invidious"
|
||||||
captcha = generate_captcha(HMAC_KEY)
|
captcha = generate_captcha(HMAC_KEY, PG_DB)
|
||||||
end
|
end
|
||||||
|
|
||||||
tfa = env.params.query["tfa"]?
|
tfa = env.params.query["tfa"]?
|
||||||
@ -815,9 +815,26 @@ post "/login" do |env|
|
|||||||
next templated "error"
|
next templated "error"
|
||||||
end
|
end
|
||||||
elsif account_type == "invidious"
|
elsif account_type == "invidious"
|
||||||
challenge_response = env.params.body["challenge_response"]?
|
answer = env.params.body["answer"]?
|
||||||
|
|
||||||
|
if !answer
|
||||||
|
error_message = "CAPTCHA is a required field"
|
||||||
|
next templated "error"
|
||||||
|
end
|
||||||
|
|
||||||
|
answer = answer.lstrip('0')
|
||||||
|
answer = OpenSSL::HMAC.hexdigest(:sha256, HMAC_KEY, answer)
|
||||||
|
|
||||||
|
challenge = env.params.body["challenge"]?
|
||||||
token = env.params.body["token"]?
|
token = env.params.body["token"]?
|
||||||
|
|
||||||
|
begin
|
||||||
|
validate_response(challenge, token, answer, "sign_in", HMAC_KEY, PG_DB)
|
||||||
|
rescue ex
|
||||||
|
error_message = ex.message
|
||||||
|
next templated "error"
|
||||||
|
end
|
||||||
|
|
||||||
action = env.params.body["action"]?
|
action = env.params.body["action"]?
|
||||||
action ||= "signin"
|
action ||= "signin"
|
||||||
|
|
||||||
@ -831,18 +848,6 @@ post "/login" do |env|
|
|||||||
next templated "error"
|
next templated "error"
|
||||||
end
|
end
|
||||||
|
|
||||||
if !challenge_response || !token
|
|
||||||
error_message = "CAPTCHA is a required field"
|
|
||||||
next templated "error"
|
|
||||||
end
|
|
||||||
|
|
||||||
challenge_response = challenge_response.lstrip('0')
|
|
||||||
if OpenSSL::HMAC.digest(:sha256, HMAC_KEY, challenge_response) == Base64.decode(token)
|
|
||||||
else
|
|
||||||
error_message = "Invalid CAPTCHA response"
|
|
||||||
next templated "error"
|
|
||||||
end
|
|
||||||
|
|
||||||
if action == "signin"
|
if action == "signin"
|
||||||
user = PG_DB.query_one?("SELECT * FROM users WHERE LOWER(email) = LOWER($1) AND password IS NOT NULL", email, as: User)
|
user = PG_DB.query_one?("SELECT * FROM users WHERE LOWER(email) = LOWER($1) AND password IS NOT NULL", email, as: User)
|
||||||
|
|
||||||
@ -940,7 +945,7 @@ get "/signout" do |env|
|
|||||||
token = env.params.query["token"]?
|
token = env.params.query["token"]?
|
||||||
|
|
||||||
begin
|
begin
|
||||||
validate_response(challenge, token, user.email, "sign_out", HMAC_KEY)
|
validate_response(challenge, token, user.email, "sign_out", HMAC_KEY, PG_DB)
|
||||||
rescue ex
|
rescue ex
|
||||||
error_message = ex.message
|
error_message = ex.message
|
||||||
next templated "error"
|
next templated "error"
|
||||||
@ -1461,7 +1466,7 @@ get "/delete_account" do |env|
|
|||||||
if user
|
if user
|
||||||
user = user.as(User)
|
user = user.as(User)
|
||||||
|
|
||||||
challenge, token = create_response(user.email, "delete_account", HMAC_KEY)
|
challenge, token = create_response(user.email, "delete_account", HMAC_KEY, PG_DB)
|
||||||
|
|
||||||
templated "delete_account"
|
templated "delete_account"
|
||||||
else
|
else
|
||||||
@ -1480,7 +1485,7 @@ post "/delete_account" do |env|
|
|||||||
token = env.params.body["token"]?
|
token = env.params.body["token"]?
|
||||||
|
|
||||||
begin
|
begin
|
||||||
validate_response(challenge, token, user.email, "delete_account", HMAC_KEY)
|
validate_response(challenge, token, user.email, "delete_account", HMAC_KEY, PG_DB)
|
||||||
rescue ex
|
rescue ex
|
||||||
error_message = ex.message
|
error_message = ex.message
|
||||||
next templated "error"
|
next templated "error"
|
||||||
@ -1506,7 +1511,7 @@ get "/clear_watch_history" do |env|
|
|||||||
if user
|
if user
|
||||||
user = user.as(User)
|
user = user.as(User)
|
||||||
|
|
||||||
challenge, token = create_response(user.email, "clear_watch_history", HMAC_KEY)
|
challenge, token = create_response(user.email, "clear_watch_history", HMAC_KEY, PG_DB)
|
||||||
|
|
||||||
templated "clear_watch_history"
|
templated "clear_watch_history"
|
||||||
else
|
else
|
||||||
@ -1525,7 +1530,7 @@ post "/clear_watch_history" do |env|
|
|||||||
token = env.params.body["token"]?
|
token = env.params.body["token"]?
|
||||||
|
|
||||||
begin
|
begin
|
||||||
validate_response(challenge, token, user.email, "clear_watch_history", HMAC_KEY)
|
validate_response(challenge, token, user.email, "clear_watch_history", HMAC_KEY, PG_DB)
|
||||||
rescue ex
|
rescue ex
|
||||||
error_message = ex.message
|
error_message = ex.message
|
||||||
next templated "error"
|
next templated "error"
|
||||||
|
@ -130,55 +130,6 @@ def login_req(login_form, f_req)
|
|||||||
return HTTP::Params.encode(data)
|
return HTTP::Params.encode(data)
|
||||||
end
|
end
|
||||||
|
|
||||||
def generate_captcha(key)
|
|
||||||
minute = Random::Secure.rand(12)
|
|
||||||
minute_angle = minute * 30
|
|
||||||
minute = minute * 5
|
|
||||||
|
|
||||||
hour = Random::Secure.rand(12)
|
|
||||||
hour_angle = hour * 30 + minute_angle.to_f / 12
|
|
||||||
if hour == 0
|
|
||||||
hour = 12
|
|
||||||
end
|
|
||||||
|
|
||||||
clock_svg = <<-END_SVG
|
|
||||||
<svg viewBox="0 0 100 100" width="200px">
|
|
||||||
<circle cx="50" cy="50" r="45" fill="#eee" stroke="black" stroke-width="2"></circle>
|
|
||||||
|
|
||||||
<text x="69" y="20.091" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 1</text>
|
|
||||||
<text x="82.909" y="34" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 2</text>
|
|
||||||
<text x="88" y="53" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 3</text>
|
|
||||||
<text x="82.909" y="72" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 4</text>
|
|
||||||
<text x="69" y="85.909" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 5</text>
|
|
||||||
<text x="50" y="91" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 6</text>
|
|
||||||
<text x="31" y="85.909" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 7</text>
|
|
||||||
<text x="17.091" y="72" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 8</text>
|
|
||||||
<text x="12" y="53" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 9</text>
|
|
||||||
<text x="17.091" y="34" text-anchor="middle" fill="black" font-family="Arial" font-size="10px">10</text>
|
|
||||||
<text x="31" y="20.091" text-anchor="middle" fill="black" font-family="Arial" font-size="10px">11</text>
|
|
||||||
<text x="50" y="15" text-anchor="middle" fill="black" font-family="Arial" font-size="10px">12</text>
|
|
||||||
|
|
||||||
<circle cx="50" cy="50" r="3" fill="black"></circle>
|
|
||||||
<line id="minute" transform="rotate(#{minute_angle}, 50, 50)" x1="50" y1="50" x2="50" y2="16" fill="black" stroke="black" stroke-width="2"></line>
|
|
||||||
<line id="hour" transform="rotate(#{hour_angle}, 50, 50)" x1="50" y1="50" x2="50" y2="24" fill="black" stroke="black" stroke-width="2"></line>
|
|
||||||
</svg>
|
|
||||||
END_SVG
|
|
||||||
|
|
||||||
challenge = ""
|
|
||||||
convert = Process.run(%(convert -density 1200 -resize 400x400 -background none svg:- png:-), shell: true,
|
|
||||||
input: IO::Memory.new(clock_svg), output: Process::Redirect::Pipe) do |proc|
|
|
||||||
challenge = proc.output.gets_to_end
|
|
||||||
challenge = Base64.strict_encode(challenge)
|
|
||||||
challenge = "data:image/png;base64,#{challenge}"
|
|
||||||
end
|
|
||||||
|
|
||||||
answer = "#{hour}:#{minute.to_s.rjust(2, '0')}"
|
|
||||||
token = OpenSSL::HMAC.digest(:sha256, key, answer)
|
|
||||||
token = Base64.urlsafe_encode(token)
|
|
||||||
|
|
||||||
return {challenge: challenge, token: token}
|
|
||||||
end
|
|
||||||
|
|
||||||
def html_to_content(description_html)
|
def html_to_content(description_html)
|
||||||
if !description_html
|
if !description_html
|
||||||
description = ""
|
description = ""
|
||||||
|
@ -200,9 +200,10 @@ def create_user(sid, email, password)
|
|||||||
return user
|
return user
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_response(user_id, operation, key, expire = 6.hours)
|
def create_response(user_id, operation, key, db, expire = 6.hours)
|
||||||
expire = Time.now + expire
|
expire = Time.now + expire
|
||||||
nonce = Random::Secure.hex(4)
|
nonce = Random::Secure.hex(16)
|
||||||
|
db.exec("INSERT INTO nonces VALUES ($1) ON CONFLICT DO NOTHING", nonce)
|
||||||
|
|
||||||
challenge = "#{expire.to_unix}-#{nonce}-#{user_id}-#{operation}"
|
challenge = "#{expire.to_unix}-#{nonce}-#{user_id}-#{operation}"
|
||||||
token = OpenSSL::HMAC.digest(:sha256, key, challenge)
|
token = OpenSSL::HMAC.digest(:sha256, key, challenge)
|
||||||
@ -213,7 +214,7 @@ def create_response(user_id, operation, key, expire = 6.hours)
|
|||||||
return challenge, token
|
return challenge, token
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate_response(challenge, token, user_id, operation, key)
|
def validate_response(challenge, token, user_id, operation, key, db)
|
||||||
if !challenge
|
if !challenge
|
||||||
raise "Hidden field \"challenge\" is a required field"
|
raise "Hidden field \"challenge\" is a required field"
|
||||||
end
|
end
|
||||||
@ -235,6 +236,12 @@ def validate_response(challenge, token, user_id, operation, key)
|
|||||||
challenge = OpenSSL::HMAC.digest(:sha256, HMAC_KEY, challenge)
|
challenge = OpenSSL::HMAC.digest(:sha256, HMAC_KEY, challenge)
|
||||||
challenge = Base64.urlsafe_encode(challenge)
|
challenge = Base64.urlsafe_encode(challenge)
|
||||||
|
|
||||||
|
if db.query_one?("SELECT EXISTS (SELECT true FROM nonces WHERE nonce = $1)", nonce, as: Bool)
|
||||||
|
db.exec("DELETE FROM nonces * WHERE nonce = $1", nonce)
|
||||||
|
else
|
||||||
|
raise "Invalid token"
|
||||||
|
end
|
||||||
|
|
||||||
if challenge != token
|
if challenge != token
|
||||||
raise "Invalid token"
|
raise "Invalid token"
|
||||||
end
|
end
|
||||||
@ -251,3 +258,53 @@ def validate_response(challenge, token, user_id, operation, key)
|
|||||||
raise "Token is expired, please try again"
|
raise "Token is expired, please try again"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def generate_captcha(key, db)
|
||||||
|
minute = Random::Secure.rand(12)
|
||||||
|
minute_angle = minute * 30
|
||||||
|
minute = minute * 5
|
||||||
|
|
||||||
|
hour = Random::Secure.rand(12)
|
||||||
|
hour_angle = hour * 30 + minute_angle.to_f / 12
|
||||||
|
if hour == 0
|
||||||
|
hour = 12
|
||||||
|
end
|
||||||
|
|
||||||
|
clock_svg = <<-END_SVG
|
||||||
|
<svg viewBox="0 0 100 100" width="200px">
|
||||||
|
<circle cx="50" cy="50" r="45" fill="#eee" stroke="black" stroke-width="2"></circle>
|
||||||
|
|
||||||
|
<text x="69" y="20.091" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 1</text>
|
||||||
|
<text x="82.909" y="34" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 2</text>
|
||||||
|
<text x="88" y="53" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 3</text>
|
||||||
|
<text x="82.909" y="72" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 4</text>
|
||||||
|
<text x="69" y="85.909" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 5</text>
|
||||||
|
<text x="50" y="91" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 6</text>
|
||||||
|
<text x="31" y="85.909" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 7</text>
|
||||||
|
<text x="17.091" y="72" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 8</text>
|
||||||
|
<text x="12" y="53" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 9</text>
|
||||||
|
<text x="17.091" y="34" text-anchor="middle" fill="black" font-family="Arial" font-size="10px">10</text>
|
||||||
|
<text x="31" y="20.091" text-anchor="middle" fill="black" font-family="Arial" font-size="10px">11</text>
|
||||||
|
<text x="50" y="15" text-anchor="middle" fill="black" font-family="Arial" font-size="10px">12</text>
|
||||||
|
|
||||||
|
<circle cx="50" cy="50" r="3" fill="black"></circle>
|
||||||
|
<line id="minute" transform="rotate(#{minute_angle}, 50, 50)" x1="50" y1="50" x2="50" y2="16" fill="black" stroke="black" stroke-width="2"></line>
|
||||||
|
<line id="hour" transform="rotate(#{hour_angle}, 50, 50)" x1="50" y1="50" x2="50" y2="24" fill="black" stroke="black" stroke-width="2"></line>
|
||||||
|
</svg>
|
||||||
|
END_SVG
|
||||||
|
|
||||||
|
image = ""
|
||||||
|
convert = Process.run(%(convert -density 1200 -resize 400x400 -background none svg:- png:-), shell: true,
|
||||||
|
input: IO::Memory.new(clock_svg), output: Process::Redirect::Pipe) do |proc|
|
||||||
|
image = proc.output.gets_to_end
|
||||||
|
image = Base64.strict_encode(image)
|
||||||
|
image = "data:image/png;base64,#{image}"
|
||||||
|
end
|
||||||
|
|
||||||
|
answer = "#{hour}:#{minute.to_s.rjust(2, '0')}"
|
||||||
|
answer = OpenSSL::HMAC.hexdigest(:sha256, key, answer)
|
||||||
|
|
||||||
|
challenge, token = create_response(answer, "sign_in", key, db)
|
||||||
|
|
||||||
|
return {image: image, challenge: challenge, token: token}
|
||||||
|
end
|
||||||
|
@ -24,10 +24,11 @@
|
|||||||
<label for="password">Password:</label>
|
<label for="password">Password:</label>
|
||||||
<input required class="pure-input-1" name="password" type="password" placeholder="Password">
|
<input required class="pure-input-1" name="password" type="password" placeholder="Password">
|
||||||
|
|
||||||
<img style="width:100%" src='<%= captcha.not_nil![:challenge] %>'/>
|
<img style="width:100%" src='<%= captcha.not_nil![:image] %>'/>
|
||||||
<input type="hidden" name="token" value="<%= captcha.not_nil![:token] %>">
|
<input type="hidden" name="token" value="<%= captcha.not_nil![:token] %>">
|
||||||
<label for="challenge_response">Time (h:mm):</label>
|
<input type="hidden" name="challenge" value="<%= captcha.not_nil![:challenge] %>">
|
||||||
<input required type="text" name="challenge_response" type="text>" placeholder="hh:mm">
|
<label for="answer">Time (h:mm):</label>
|
||||||
|
<input required type="text" name="answer" type="text>" placeholder="hh:mm">
|
||||||
|
|
||||||
<button type="submit" name="action" value="signin" class="pure-button pure-button-primary">Sign In</button>
|
<button type="submit" name="action" value="signin" class="pure-button pure-button-primary">Sign In</button>
|
||||||
<button type="submit" name="action" value="register" class="pure-button pure-button-primary">Register</button>
|
<button type="submit" name="action" value="register" class="pure-button pure-button-primary">Register</button>
|
||||||
|
Loading…
Reference in New Issue
Block a user