d27769317be7f92e199f69d75b10a7421eecf770
max
  Sat Sep 26 21:57:15 2026 -0700
hgGeneGraph: require a form submission and a checked answer to file a feedback report

diff --git src/hg/hgGeneGraph/hgGeneGraph src/hg/hgGeneGraph/hgGeneGraph
index 7ee374f979d..0f87b36bf9d 100755
--- src/hg/hgGeneGraph/hgGeneGraph
+++ src/hg/hgGeneGraph/hgGeneGraph
@@ -19,31 +19,31 @@
 # dark blue, dashed = only low-throughput data
 
 # dark blue, thickness = low-throughput data + text
 # dark blue + dashed  = only pathway data
 
 # code review
 # - os.system is not a security risk here, no variables go into the cmd line
 # - mysql statements are not escaped, instead all CGI vars are checked for non-alpha letters
 
 # hgFixed tables required for this script: ggLink (main table with gene-gene links), 
 # ggLinkEvent (details about link), ggEventDb (details about links from databases), 
 # ggEventText (details about links from text mining), ggDoc (details about documents for ggEventText)
 # ggGeneName (symbols), ggGeneClass (HPRD/Panther class)
 
 # these are default python modules on python 2.7, no errors expected here
-import sys, cgi, os, string, urllib.request, urllib.parse, urllib.error, operator, hashlib, html
+import sys, cgi, os, string, urllib.request, urllib.parse, urllib.error, operator, hashlib, html, random
 from sys import exit
 from collections import defaultdict, namedtuple
 from os.path import *
 
 #These two lines should be commented out whenever committing this file into git.
 #They activate stack traces to stdout which can in certain cases reveal the mysql password,
 #when there is a Mysql connection problem. 
 #import cgitb
 #cgitb.enable()
 
 # import the UCSC-specific library
 sys.path.append(join(dirname(__file__), "pyLib"))
 try:
     from hgLib3 import cgiArgs, cgiSetup, cgiString, printContentType, printMenuBar, \
             sqlConnect, sqlQuery, errAbort, cfgOption, runCmd, cgiGetAll, printHgcHeader, \
@@ -1880,57 +1880,69 @@
 
     print("<h3>Interaction %s - %s</h3>" % (causeGene, themeGene))
 
     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="GET" action="%s">' % basename(__file__)))
+    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.
+    numA = random.randint(1, 9)
+    numB = random.randint(1, 9)
+    print(('<INPUT TYPE="HIDDEN" NAME="captchaHash" VALUE="%s"></INPUT>' % saltedHash(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, comment):
+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:
+        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));")
 
     # name the columns: the positional version put NOW() into "comment" and the comment text
@@ -2006,32 +2018,33 @@
         print("<li>%s: %d" % (db, len(pairSet)))
     print("</ul>")
 
 def htmlMiddle():
     " print html middle part "
     sys.stdout.flush()
 
     flag = getCgiVar("flag")
     if flag!=None:
         flagInteraction(flag)
         exit(0)
         
     remove = getCgiVar("remove")
     if remove!=None:
         captcha = getCgiVar("captcha")
+        captchaHash = getCgiVar("captchaHash", maxLen=40)
         comment = getCgiVar("comment", allowAnyChar=True, maxLen=10000)
-        removeInteraction(remove, captcha, comment)
+        removeInteraction(remove, captcha, captchaHash, comment)
         exit(0)
         
     docId = getCgiVar("docId")
     if docId!=None:
         showDoc(docId)
         exit(0)
         
     link = getCgiVar("link")
     if link!=None:
         showLink(link)
         exit(0)
 
     page = getCgiVar("page")
     if page=="stats":
         showStats()