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 '%s' % (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 ("

") print ("Thank you for reporting errors, e.g.") print("

") print("This makes it easier for us to improve the text mining system or database imports.
") print("You can leave your email address in the comment if you want to give us the possibility to comment.

") print ("Optional comment:
") print(('

' % basename(__file__))) print(('' % 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(('' % saltedHash(str(numA+numB)))) + print(('' % captchaHashFor(param, str(numA+numB)))) print ('') print ("

") print ('What is %d + %d?

' % (numA, numB)) print ('') print ('
') #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));")