shugoi 0.4.2 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/shugoi/core.rb +6 -5
- data/lib/shugoi/pow.rb +55 -21
- data/lib/shugoi/rack/middleware.rb +1 -1
- data/lib/shugoi/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ae02598e982619825522554b0190f1a1272acad33f637a978d0e821eb0861c34
|
|
4
|
+
data.tar.gz: ec6dbe09ad713c8bbee63755b6b9bf3f048d201aed2ec1d104c15660b3f80da5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 50fdb144de09568d85efeacf3fabc166459ecc9c2b95004432493325310188a41ef8c2544dae2773317de34ce66b438f0d69a76cbca3be862038c969a6766c63
|
|
7
|
+
data.tar.gz: e8332144c9d514bf52f37df0d829db7a3bcfd5f7232a7899a82a80ac498ae705b33edda25d55f5959e285713b1037c10214aeeadc5c47174e2b6f4ee87dc3ae6
|
data/lib/shugoi/core.rb
CHANGED
|
@@ -59,7 +59,7 @@ module Shugoi
|
|
|
59
59
|
valid_proof = !proof.empty? && @pow.valid?(proof)
|
|
60
60
|
# Re-audit (résidu #3) : un cookie __sg_ok valide (HMAC serveur, 30 j) saute le
|
|
61
61
|
# pre-flight PoW. Posé APRÈS une première résolution réussie (middleware).
|
|
62
|
-
valid_cookie = !ctx[:sg_ok].to_s.empty? && @pow.sg_ok_valid?(ctx[:sg_ok])
|
|
62
|
+
valid_cookie = !ctx[:sg_ok].to_s.empty? && @pow.sg_ok_valid?(ctx[:sg_ok], ctx[:ip].to_s, ua)
|
|
63
63
|
# Round 16 (R2) : la preuve est SINGLE-USE (par IP) — un rejeu → 307.
|
|
64
64
|
proof_fresh = valid_proof ? consume_proof(proof, ctx[:ip].to_s) : false
|
|
65
65
|
unless valid_cookie || proof_fresh
|
|
@@ -128,7 +128,7 @@ module Shugoi
|
|
|
128
128
|
js = <<~JS
|
|
129
129
|
(function(){
|
|
130
130
|
var P=new URLSearchParams(location.search);
|
|
131
|
-
var salt=P.get('salt')||'', ts=P.get('ts')||'', diff=parseInt(P.get('diff')||'#{@config.pow_difficulty}',10), path=P.get('path')||'/';
|
|
131
|
+
var salt=P.get('salt')||'', ts=P.get('ts')||'', nonce=P.get('nonce')||'', diff=parseInt(P.get('diff')||'#{@config.pow_difficulty}',10), path=P.get('path')||'/';
|
|
132
132
|
// Open redirect (audit #5) : un //evil.com (protocole-relatif) ou un backslash
|
|
133
133
|
// détourneraient le location.replace ci-dessous vers un domaine externe.
|
|
134
134
|
if(path.charAt(0)!=='/'||path.charAt(1)==='/'||path.indexOf('\\\\')>=0)path='/';
|
|
@@ -140,7 +140,7 @@ module Shugoi
|
|
|
140
140
|
function step(){
|
|
141
141
|
crypto.subtle.digest('SHA-256',enc.encode(salt+':'+n.toString(16))).then(function(buf){
|
|
142
142
|
var h=Array.from(new Uint8Array(buf)).map(function(v){return v.toString(16).padStart(2,'0')}).join('');
|
|
143
|
-
if(bits(h)>=diff){var base=path;var q=(base.indexOf('?')>=0?'&':'?')+'sg_proof='+ts+':'+n.toString(16);location.replace(base+q)}
|
|
143
|
+
if(bits(h)>=diff){var base=path;var q=(base.indexOf('?')>=0?'&':'?')+'sg_proof='+ts+':'+nonce+':'+n.toString(16);location.replace(base+q)}
|
|
144
144
|
else{n++;if(n<300000)step()}
|
|
145
145
|
}).catch(function(){location.reload()});
|
|
146
146
|
}
|
|
@@ -174,12 +174,13 @@ module Shugoi
|
|
|
174
174
|
# 307 vers le challenge : body = tableau ASCII seul (curl le voit tel quel).
|
|
175
175
|
def pow_challenge_307(ctx)
|
|
176
176
|
ts = Utils.now_sec
|
|
177
|
-
|
|
177
|
+
nonce = SecureRandom.hex(8)
|
|
178
|
+
salt = Utils.hmac_hex(@config.signing_secret, "#{ts}:#{nonce}")
|
|
178
179
|
prefix = ctx[:forwarded_prefix].to_s
|
|
179
180
|
prefix = "" if prefix == "/"
|
|
180
181
|
prefix = prefix.sub(%r{/\z}, "") unless prefix.empty?
|
|
181
182
|
path = safe_challenge_path(ctx[:path].to_s)
|
|
182
|
-
chal_url = "#{prefix}/__sg_challenge?ts=#{ts}&salt=#{salt}&diff=#{@config.pow_difficulty}&path=#{URI.encode_www_form_component(prefix + path)}"
|
|
183
|
+
chal_url = "#{prefix}/__sg_challenge?ts=#{ts}&salt=#{salt}&nonce=#{nonce}&diff=#{@config.pow_difficulty}&path=#{URI.encode_www_form_component(prefix + path)}"
|
|
183
184
|
Decision.new(status: 307, content_type: "text/plain", body: BLOCK_PAGE, headers: { "location" => chal_url })
|
|
184
185
|
end
|
|
185
186
|
|
data/lib/shugoi/pow.rb
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
3
5
|
module Shugoi
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
# Proof-of-work anti-curl + cookies HMAC — parité avec core.ts (module Node) :
|
|
7
|
+
# salt = HMAC(secret, ts + ':' + nonce) (nonce 64 bits ALEATOIRE par challenge)
|
|
8
|
+
# proof = "ts:nonce:solution" où SHA256(salt:solution) a >= POW_DIFFICULTY bits à zéro.
|
|
9
|
+
# __sg_ok : ts:ipBucket:uaFp:HMAC(secret, "sg_ok:ts:ipBucket:uaFp") — 30 jours,
|
|
10
|
+
# lié au bucket IP + empreinte UA (non rejouable depuis une autre IP),
|
|
11
|
+
# saute le pre-flight PoW.
|
|
12
|
+
# __sg_authorized: ts:HMAC(secret, "sg_authorized:ts") — 120 s, protège les assets /assets/*
|
|
9
13
|
class Pow
|
|
10
14
|
POW_OK_TTL_MS = 30 * 24 * 3600 * 1000
|
|
11
15
|
AUTHORIZED_TTL_MS = 120_000
|
|
@@ -19,48 +23,53 @@ module Shugoi
|
|
|
19
23
|
# Génère le challenge à injecter (window.__sg_pow).
|
|
20
24
|
def challenge
|
|
21
25
|
ts = Utils.now_sec
|
|
22
|
-
{ ts: ts, salt: salt(ts), difficulty: @difficulty }
|
|
26
|
+
{ ts: ts, nonce: nonce, salt: salt(ts, nonce), difficulty: @difficulty }
|
|
23
27
|
end
|
|
24
28
|
|
|
25
|
-
# Vérifie un proof "ts:nonce" (fenêtre @ttl_ms, comptage de bits CORRIGÉ).
|
|
29
|
+
# Vérifie un proof "ts:nonce:solution" (fenêtre @ttl_ms, comptage de bits CORRIGÉ).
|
|
26
30
|
def valid?(proof)
|
|
27
31
|
return false if proof.to_s.empty? || @secret.empty?
|
|
28
|
-
ts_str, solution = proof.to_s.split(":",
|
|
29
|
-
return false if ts_str.nil? || solution.nil?
|
|
32
|
+
ts_str, nonce, solution = proof.to_s.split(":", 3)
|
|
33
|
+
return false if ts_str.nil? || nonce.nil? || solution.nil?
|
|
34
|
+
return false unless nonce.match?(/\A[0-9a-f]{16}\z/)
|
|
30
35
|
|
|
31
36
|
ts = ts_str.to_i
|
|
32
37
|
return false if ts.zero?
|
|
33
38
|
return false if (Utils.now_ms - ts * 1000).abs > @ttl_ms
|
|
34
39
|
|
|
35
|
-
digest = Utils.sha256_hex("#{salt(ts_str)}:#{solution}")
|
|
40
|
+
digest = Utils.sha256_hex("#{salt(ts_str, nonce)}:#{solution}")
|
|
36
41
|
Utils.leading_zero_bits(digest) >= @difficulty
|
|
37
42
|
end
|
|
38
43
|
|
|
39
|
-
# Valeur du cookie __sg_ok (HMAC serveur, 30 j).
|
|
40
|
-
def sg_ok_value
|
|
44
|
+
# Valeur du cookie __sg_ok (HMAC serveur, 30 j) — lié au bucket IP + empreinte UA.
|
|
45
|
+
def sg_ok_value(ip = "", ua = "")
|
|
41
46
|
ts = Utils.now_sec
|
|
42
|
-
|
|
47
|
+
bucket = ip_bucket(ip)
|
|
48
|
+
fp = ua_fp(ua)
|
|
49
|
+
"#{ts}:#{bucket}:#{fp}:#{Utils.hmac_hex(@secret, "sg_ok:#{ts}:#{bucket}:#{fp}")}"
|
|
43
50
|
end
|
|
44
51
|
|
|
45
|
-
def sg_ok_valid?(cookie_val)
|
|
52
|
+
def sg_ok_valid?(cookie_val, ip = "", ua = "")
|
|
46
53
|
return false if @secret.empty? || cookie_val.to_s.empty?
|
|
47
|
-
ts_str, sig = cookie_val.to_s.split(":",
|
|
48
|
-
return false if ts_str.nil? || sig.nil?
|
|
54
|
+
ts_str, bucket, fp, sig = cookie_val.to_s.split(":", 4)
|
|
55
|
+
return false if ts_str.nil? || bucket.nil? || fp.nil? || sig.nil?
|
|
49
56
|
|
|
50
57
|
ts = ts_str.to_i
|
|
51
58
|
return false if ts.zero?
|
|
52
59
|
return false if Utils.now_ms - ts * 1000 > POW_OK_TTL_MS
|
|
53
60
|
return false if ts * 1000 > Utils.now_ms + 60_000
|
|
61
|
+
# Lier au bucket IP + UA courants : un cookie d'une autre IP/UA → invalide.
|
|
62
|
+
return false unless bucket == ip_bucket(ip) && fp == ua_fp(ua)
|
|
54
63
|
|
|
55
|
-
Utils.secure_equals(sig, Utils.hmac_hex(@secret, "sg_ok:#{ts_str}"))
|
|
64
|
+
Utils.secure_equals(sig, Utils.hmac_hex(@secret, "sg_ok:#{ts_str}:#{bucket}:#{fp}"))
|
|
56
65
|
end
|
|
57
66
|
|
|
58
67
|
# String Set-Cookie pour __sg_ok (posé par le middleware après une preuve valide).
|
|
59
68
|
# @return [String, nil] nil si la preuve n'est pas valide
|
|
60
|
-
def sg_ok_cookie(proof)
|
|
69
|
+
def sg_ok_cookie(proof, ip = "", ua = "")
|
|
61
70
|
return nil unless valid?(proof)
|
|
62
71
|
secure = production? ? "; Secure" : ""
|
|
63
|
-
"__sg_ok=#{sg_ok_value}; Path=/; HttpOnly; SameSite=Lax; Max-Age=#{POW_OK_TTL_MS / 1000}#{secure}"
|
|
72
|
+
"__sg_ok=#{sg_ok_value(ip, ua)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=#{POW_OK_TTL_MS / 1000}#{secure}"
|
|
64
73
|
end
|
|
65
74
|
|
|
66
75
|
# Cookie __sg_authorized posé par handleRender après un render réussi (grant valide).
|
|
@@ -89,8 +98,33 @@ module Shugoi
|
|
|
89
98
|
|
|
90
99
|
private
|
|
91
100
|
|
|
92
|
-
|
|
93
|
-
|
|
101
|
+
# Nonce 64 bits ALEATOIRE par challenge (plus de sel déterministe par seconde →
|
|
102
|
+
# précomputation par lots impossible). Parité core.ts sgNonce().
|
|
103
|
+
def nonce
|
|
104
|
+
SecureRandom.hex(8)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def salt(ts, nonce = "")
|
|
108
|
+
Utils.hmac_hex(@secret, "#{ts}:#{nonce}")
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Bucket d'IP (sans ':' pour rester parseable dans le cookie). IPv4 → /24,
|
|
112
|
+
# IPv6 → 4 hextets. Parité core.ts ipBucket().
|
|
113
|
+
def ip_bucket(ip)
|
|
114
|
+
ip = ip.to_s
|
|
115
|
+
return "0" if ip.empty? || ip == "unknown"
|
|
116
|
+
if ip.include?(".")
|
|
117
|
+
m = ip.match(/\A(\d+\.\d+\.\d+)(?:\.\d+)?\z/)
|
|
118
|
+
return m ? m[1] : "0"
|
|
119
|
+
end
|
|
120
|
+
return "0" unless ip.include?(":")
|
|
121
|
+
segs = ip.split(":").reject(&:empty?)
|
|
122
|
+
segs.first(4).join(".").empty? ? "0" : segs.first(4).join(".")
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Empreinte UA (16 hex). Parité core.ts uaFp().
|
|
126
|
+
def ua_fp(ua)
|
|
127
|
+
Utils.sha256_hex(ua.to_s).slice(0, 16)
|
|
94
128
|
end
|
|
95
129
|
|
|
96
130
|
def production?
|
|
@@ -83,7 +83,7 @@ module Shugoi
|
|
|
83
83
|
|
|
84
84
|
# PoW validé → pose le cookie __sg_ok (navigations suivantes sans challenge).
|
|
85
85
|
if (proof = query["sg_proof"]) && !decision
|
|
86
|
-
if (ok_cookie = @pow.sg_ok_cookie(proof))
|
|
86
|
+
if (ok_cookie = @pow.sg_ok_cookie(proof, ctx[:ip].to_s, ctx[:ua].to_s))
|
|
87
87
|
resp_headers["set-cookie"] = ok_cookie
|
|
88
88
|
end
|
|
89
89
|
end
|
data/lib/shugoi/version.rb
CHANGED