b6f15357f432f1c218fabcb0d53777322e357c00 max Tue Sep 29 15:43:57 2026 -0700 hgGeneGraph: bind the feedback-form arithmetic check to its target and a real secret Reuses a per-render hash instead of a fixed public constant, and ties it to the specific gene pair rather than just the answer, refs #38399 diff --git src/hg/hgGeneGraph/hgGeneGraph src/hg/hgGeneGraph/hgGeneGraph index 0f87b36bf9d..5fcc08b921c 100755 --- src/hg/hgGeneGraph/hgGeneGraph +++ src/hg/hgGeneGraph/hgGeneGraph @@ -318,30 +318,39 @@ if style!=None: styleStr='style="%s" ' % style titleStr = "" if title!=None: titleStr = ' title="%s"' % title.replace('"', ' ') return '<a %s%s%shref="%s"%s>%s</a>' % (titleStr, classStr, dataToggleStr, url, styleStr, linkName) def saltedHash(word, length=5): " return first 5 chars of salted hash " # pretty simple salt: PITX2, salting is just for the captcha inStr = word+"PITX2" hashStr = "".join(hashlib.sha1(inStr.encode("utf8")).hexdigest()[:length]).lower() return hashStr +def captchaHashFor(param, answer): + """ Hash of an arithmetic-captcha answer, bound to the specific removal target and a + secret from hg.conf.private, so one solved answer is only good for the one form it was + shown on. param is length-prefixed so a differently-split (param, answer) pair cannot + concatenate to the same bytes and collide. """ + secret = cfgOption("hgGeneGraph.captchaSecret", "PITX2") + inStr = "%s%d:%s%s" % (secret, len(param), param, answer) + return hashlib.sha1(inStr.encode("utf8")).hexdigest()[:10] + def reqMinSupp(links, minArtSupp, maxResCount, targetGene): """ remove all 'text mining only' links with less than minArtSupp supporting documents The only exception is targetGene which we always want to stay connected Also remove links that are PPI-only and have a high minResCount. """ newLinks = defaultdict(set) genes = set() targetConns = {} for genePair, linkData in links.items(): docCount, dbCount, tagSet, pairMinResCount = linkData[:4] if targetGene in genePair: targetConns[genePair] = linkData # remove text-mining links with only one article @@ -1882,65 +1891,65 @@ conn = sqlConnect(GGDB) rows = queryEventText(conn, causeGene, themeGene) print ("<p>") print ("Thank you for reporting errors, e.g.") print("<ul><li>not true (e.g. the authors state that the interaction does not happen )</li>") print("<li>text mining errors (the authors did not say anything about this particular interaction)</li>") print("</ul>") print("This makes it easier for us to improve the text mining system or database imports.<br>") print("You can leave your email address in the comment if you want to give us the possibility to comment.<P>") print ("Optional comment: <br>") print(('<FORM method="POST" action="%s">' % basename(__file__))) print(('<INPUT TYPE="HIDDEN" NAME="remove" VALUE="%s"></INPUT>' % param)) - # A short arithmetic question, checked back in removeInteraction() against the salted - # hash of its answer, keeps this write-capable form from being submitted from anywhere - # but this page. + # A short arithmetic question, checked back in removeInteraction() against a hash of its + # answer and this target, keeps this write-capable form from being submitted from anywhere + # but this page, or replayed against a different target. numA = random.randint(1, 9) numB = random.randint(1, 9) - print(('<INPUT TYPE="HIDDEN" NAME="captchaHash" VALUE="%s"></INPUT>' % saltedHash(str(numA+numB)))) + print(('<INPUT TYPE="HIDDEN" NAME="captchaHash" VALUE="%s"></INPUT>' % captchaHashFor(param, str(numA+numB)))) print ('<TEXTAREA rows="6" cols="50" name="comment"></TEXTAREA>') print ("<P>") print ('What is %d + %d? <INPUT TYPE="TEXT" NAME="captcha" SIZE="4"></INPUT></P>' % (numA, numB)) print ('<INPUT TYPE="SUBMIT" name="submit" value="Report Interaction as inaccurate"></INPUT>') print ('</FORM>') #printMsrNlpRows(rows) showSnipsLink(conn, causeGene, themeGene) #def namedtuple_factory(cursor, row): #""" #used as a function pointer to sqlite to have it return structs with names and not just arrays #""" #fields = [col[0] for col in cursor.description] #Row = collections.namedtuple("Row", fields) #return Row(*row) #def openSqlite(dbName): #" opens sqlite database and have it return structs (=namedtuples) " #tryCount = retries #con = None #con = sqlite3.connect(dbName, timeout=20) #con.row_factory = namedtuple_factory ##con.row_factory = sqlite3.Row def removeInteraction(param, captcha, captchaHash, comment): if os.environ.get("REQUEST_METHOD") != "POST": errAbort("This action requires a form submission, not a link.") - if captcha is None or captchaHash is None or saltedHash(captcha.strip()) != captchaHash: + if captcha is None or captchaHash is None or captchaHashFor(param, captcha.strip()) != captchaHash: errAbort("The answer to the arithmetic question was not correct. Please go back and try again.") fields = param.split(":") if len(fields)!=2: errAbort( "illegal CGI parameter") causeGene, themeGene = fields ip = html.escape(os.environ["REMOTE_ADDR"]) conn = hConnectCentral() if not sqlTableExists(conn, "ggFeedback"): # sqlQuery() reads cursor.description, which is None after a statement that returns no # rows, so it cannot run a CREATE sqlUpdate(conn, "CREATE TABLE ggFeedback (causeGene varchar(255), themeGene varchar(255), pmid varchar(255), " \ "comment varchar(10000), time TIMESTAMP, ip varchar(255), INDEX allIdx (causeGene, themeGene, pmid));")