Skip to content

Commit aa3501e

Browse files
v0.16.1 (see NEWS)
1 parent a3aef45 commit aa3501e

File tree

18 files changed

+211
-79
lines changed

18 files changed

+211
-79
lines changed

NEWS

+11
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,17 @@
1212
██║ ╚████║███████╗╚███╔███╔╝███████║
1313
╚═╝ ╚═══╝╚══════╝ ╚══╝╚══╝ ╚══════╝
1414

15+
v0.16.1 (development) Sun Mar 24 10:36:47 AM CET 2024
16+
--------------------------------------------------------
17+
- Minor features:
18+
- Protect the two main lookup pages with a cookie check against
19+
leeching attacks
20+
- Update to Play 2.9.2, ScalaJS 1.16.0 and Scala 2.13.13
21+
22+
- Bug fixes:
23+
- Sorting in file modal dialogue broke with the removal of RX
24+
libraries and is fixed now
25+
1526
v0.16.0 (development) Sa 13. Jan 14:28:21 CET 2024
1627
--------------------------------------------------------
1728
- Main features:

backend/app/org/multics/baueran/frep/backend/controllers/Get.scala

+101-47
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ class Get @Inject()(cc: ControllerComponents, dbContext: DBContext) extends Abst
6060
Cookie(CookieFields.id.toString, member.member_id.toString, secure = true, httpOnly = false),
6161
Cookie(CookieFields.cookiePopupAccepted.toString, "1", secure = true, httpOnly = false)
6262
)
63+
.withSession("id" -> member.member_id.toString)
6364
}
6465
}
6566

@@ -78,7 +79,14 @@ class Get @Inject()(cc: ControllerComponents, dbContext: DBContext) extends Abst
7879

7980
def show(repertory: String, symptom: String, page: Int, remedyString: String, minWeight: Int) = Action { implicit request: Request[AnyContent] =>
8081
try {
81-
Ok(views.html.index_lookup(request, repertory, URLEncoder.encode(symptom, StandardCharsets.UTF_8.toString()), page - 1, remedyString, minWeight, s"OOREP - ${symptom} (${repertory})"))
82+
getAuthenticatedUser(request) match {
83+
case None =>
84+
Ok(views.html.index_lookup(request, repertory, URLEncoder.encode(symptom, StandardCharsets.UTF_8.toString()), page - 1, remedyString, minWeight, s"OOREP - ${symptom} (${repertory})"))
85+
.withSession("id" -> "-1")
86+
case Some(member) =>
87+
Ok(views.html.index_lookup(request, repertory, URLEncoder.encode(symptom, StandardCharsets.UTF_8.toString()), page - 1, remedyString, minWeight, s"OOREP - ${symptom} (${repertory})"))
88+
.withSession("id" -> member.member_id.toString)
89+
}
8290
} catch {
8391
case e: Exception =>
8492
Logger.debug(s"GET: show() failed; most likely URLEncoder.encode(): ${e.toString}")
@@ -88,7 +96,14 @@ class Get @Inject()(cc: ControllerComponents, dbContext: DBContext) extends Abst
8896

8997
def showMM(materiaMedica: String, symptom: String, page: Int, hideSections: Boolean, remedyString: String) = Action { implicit request: Request[AnyContent] =>
9098
try {
91-
Ok(views.html.index_lookup_mm(request, materiaMedica, URLEncoder.encode(symptom, StandardCharsets.UTF_8.toString()), page - 1, hideSections, remedyString, s"OOREP - ${symptom} (${materiaMedica})"))
99+
getAuthenticatedUser(request) match {
100+
case None =>
101+
Ok(views.html.index_lookup_mm(request, materiaMedica, URLEncoder.encode(symptom, StandardCharsets.UTF_8.toString()), page - 1, hideSections, remedyString, s"OOREP - ${symptom} (${materiaMedica})"))
102+
.withSession("id" -> "-1")
103+
case Some(member) =>
104+
Ok(views.html.index_lookup_mm(request, materiaMedica, URLEncoder.encode(symptom, StandardCharsets.UTF_8.toString()), page - 1, hideSections, remedyString, s"OOREP - ${symptom} (${materiaMedica})"))
105+
.withSession("id" -> member.member_id.toString)
106+
}
92107
} catch {
93108
case e: Exception =>
94109
Logger.debug(s"GET: showMM() failed; most likely URLEncoder.encode(): ${e.toString}")
@@ -160,15 +175,36 @@ class Get @Inject()(cc: ControllerComponents, dbContext: DBContext) extends Abst
160175
}
161176

162177
def apiAvailableRemedies() = Action { request: Request[AnyContent] =>
163-
Ok(repertoryDao.getRemedies().asJson.toString())
178+
getAuthenticatedUser(request) match {
179+
case Some(member) =>
180+
Ok(repertoryDao.getRemedies().asJson.toString())
181+
.withSession("id" -> member.member_id.toString)
182+
case None =>
183+
Ok(repertoryDao.getRemedies().asJson.toString())
184+
.withSession("id" -> "-1")
185+
}
164186
}
165187

166188
def apiAvailableRepertoriesAndRemedies() = Action { request: Request[AnyContent] =>
167-
Ok((repertoryDao.getRepsAndRemedies(getAuthenticatedUser(request)).asJson.toString))
189+
getAuthenticatedUser(request) match {
190+
case Some(member) =>
191+
Ok((repertoryDao.getRepsAndRemedies(getAuthenticatedUser(request)).asJson.toString))
192+
.withSession("id" -> member.member_id.toString)
193+
case None =>
194+
Ok((repertoryDao.getRepsAndRemedies(getAuthenticatedUser(request)).asJson.toString))
195+
.withSession("id" -> "-1")
196+
}
168197
}
169198

170199
def apiAvailableMateriaMedicasAndRemedies() = Action { request: Request[AnyContent] =>
171-
Ok(mmDao.getMMsAndRemedies(getAuthenticatedUser(request)).asJson.toString())
200+
getAuthenticatedUser(request) match {
201+
case Some(member) =>
202+
Ok(mmDao.getMMsAndRemedies(getAuthenticatedUser(request)).asJson.toString())
203+
.withSession("id" -> member.member_id.toString)
204+
case None =>
205+
Ok(mmDao.getMMsAndRemedies(getAuthenticatedUser(request)).asJson.toString())
206+
.withSession("id" -> "-1")
207+
}
172208
}
173209

174210
/**
@@ -282,57 +318,75 @@ class Get @Inject()(cc: ControllerComponents, dbContext: DBContext) extends Abst
282318
}
283319
}
284320

285-
def apiLookupRep(repertoryAbbrev: String, symptom: String, page: Int, remedyString: String, minWeight: Int, getRemedies: Int) = Action { request: Request[AnyContent] =>
286-
// We don't allow '*' in the middle of a search term. '*' can only be at beginning or end of a word, whether exact search term or not.
287-
if (symptom.trim.matches(".*\\w+\\*\\w+.*") || symptom.trim.contains(" * ")) {
288-
NoContent
289-
} else {
290-
val searchTerms = new SearchTerms(symptom.trim)
291-
val cleanedUpAbbrev = repertoryAbbrev.replaceAll("[^0-9A-Za-z\\-]", "")
292-
293-
// Check if user is allowed to access the resource at all (might be Private or Protected and user not logged in)
294-
if (repertoryDao.getRepsAndRemedies(getAuthenticatedUser(request)).find(_.info.abbrev == cleanedUpAbbrev) == None) {
295-
Logger.info(s"Get: apiLookupRep(abbrev: ${repertoryAbbrev}, symptom: ${symptom}, page: ${page}, remedy: ${remedyString}, weight: ${minWeight}): user not allowed to access ressource.")
296-
NoContent
297-
}
298-
else {
299-
// Do actual look-up and return results in case of success.
300-
repertoryDao.queryRepertory(cleanedUpAbbrev, searchTerms, page, remedyString.trim, minWeight, getRemedies != 0) match {
301-
case Some((ResultsCaseRubrics(totalNumberOfRepertoryRubrics, totalNumberOfResults, totalNumberOfPages, page, results), remedyStats)) if (totalNumberOfPages > 0) =>
302-
Ok((ResultsCaseRubrics(totalNumberOfRepertoryRubrics, totalNumberOfResults, totalNumberOfPages, page, results), remedyStats).asJson.toString())
303-
case _ =>
304-
Logger.info(s"Get: apiLookupRep(abbrev: ${repertoryAbbrev}, symptom: ${symptom}, page: ${page}, remedy: ${remedyString}, weight: ${minWeight}): no results found")
321+
private def isCrossSiteRequest(request: Request[AnyContent]): Boolean = {
322+
request.session.get("id") == None && getAuthenticatedUser(request) == None
323+
}
324+
325+
def apiLookupRep(repertoryAbbrev: String, symptom: String, page: Int, remedyString: String, minWeight: Int, getRemedies: Int): Action[AnyContent] =
326+
Action { request: Request[AnyContent] =>
327+
if (isCrossSiteRequest(request)) {
328+
val errStr = (s"ERROR: request to ${request.uri} not authorized. Make sure your browser allows cookies. (IP: ${request.remoteAddress})")
329+
Logger.error(errStr)
330+
Unauthorized(errStr)
331+
} else {
332+
// We don't allow '*' in the middle of a search term. '*' can only be at beginning or end of a word, whether exact search term or not.
333+
if (symptom.trim.matches(".*\\w+\\*\\w+.*") || symptom.trim.contains(" * ")) {
334+
NoContent
335+
} else {
336+
val searchTerms = new SearchTerms(symptom.trim)
337+
val cleanedUpAbbrev = repertoryAbbrev.replaceAll("[^0-9A-Za-z\\-]", "")
338+
339+
// Check if user is allowed to access the resource at all (might be Private or Protected and user not logged in)
340+
if (repertoryDao.getRepsAndRemedies(getAuthenticatedUser(request)).find(_.info.abbrev == cleanedUpAbbrev) == None) {
341+
Logger.warn(s"Get: apiLookupRep(abbrev: ${repertoryAbbrev}, symptom: ${symptom}, page: ${page}, remedy: ${remedyString}, weight: ${minWeight}): user not allowed to access ressource.")
305342
NoContent
343+
}
344+
else {
345+
// Do actual look-up and return results in case of success.
346+
repertoryDao.queryRepertory(cleanedUpAbbrev, searchTerms, page, remedyString.trim, minWeight, getRemedies != 0) match {
347+
case Some((ResultsCaseRubrics(totalNumberOfRepertoryRubrics, totalNumberOfResults, totalNumberOfPages, page, results), remedyStats)) if (totalNumberOfPages > 0) =>
348+
Ok((ResultsCaseRubrics(totalNumberOfRepertoryRubrics, totalNumberOfResults, totalNumberOfPages, page, results), remedyStats).asJson.toString())
349+
case _ =>
350+
Logger.info(s"Get: apiLookupRep(abbrev: ${repertoryAbbrev}, symptom: ${symptom}, page: ${page}, remedy: ${remedyString}, weight: ${minWeight}): no results found")
351+
NoContent
352+
}
353+
}
306354
}
307355
}
308356
}
309-
}
310357

311-
def apiLookupMM(mmAbbrev: String, symptom: String, page: Int, remedyString: String) = Action { request: Request[AnyContent] =>
312-
// We don't allow '*' in the middle of a search term. '*' can only be at beginning or end of a word, whether exact search term or not.
313-
if (symptom.trim.matches(".*\\w+\\*\\w+.*") || symptom.trim.contains(" * ")) {
314-
NoContent
315-
} else {
316-
val searchTerms = new SearchTerms(symptom.trim)
317-
val cleanedUpAbbrev = mmAbbrev.replaceAll("[^0-9A-Za-z\\-]", "")
318-
319-
// Check if user is allowed to access the resource at all (might be Private or Protected and user not logged in)
320-
if (mmDao.getMMsAndRemedies(getAuthenticatedUser(request)).find(_.mminfo.abbrev == cleanedUpAbbrev) == None) {
321-
Logger.info(s"Get: apiLookupMM(abbrev: ${mmAbbrev}, symptom: ${symptom}, page: ${page}, remedy: ${remedyString}): user not allowed to access ressource.")
322-
NoContent
323-
}
324-
else {
325-
// Do actual look-up and return results in case of success.
326-
mmDao.getSectionHits(cleanedUpAbbrev, searchTerms, page, Some(remedyString)) match {
327-
case Some(sectionHits) if (sectionHits.results.length > 0 || sectionHits.numberOfMatchingSectionsPerChapter.length > 0) =>
328-
Ok(sectionHits.asJson.toString())
329-
case _ =>
330-
Logger.info(s"Get: apiLookupMM(abbrev: ${mmAbbrev}, symptom: ${symptom}, page: ${page}, remedy: ${remedyString}): no results found")
358+
def apiLookupMM(mmAbbrev: String, symptom: String, page: Int, remedyString: String): Action[AnyContent] =
359+
Action { request: Request[AnyContent] =>
360+
if (isCrossSiteRequest(request)) {
361+
val errStr = (s"ERROR: request to ${request.uri} not authorized. Make sure your browser allows cookies. (IP: ${request.remoteAddress})")
362+
Logger.error(errStr)
363+
Unauthorized(errStr)
364+
} else {
365+
// We don't allow '*' in the middle of a search term. '*' can only be at beginning or end of a word, whether exact search term or not.
366+
if (symptom.trim.matches(".*\\w+\\*\\w+.*") || symptom.trim.contains(" * ")) {
367+
NoContent
368+
} else {
369+
val searchTerms = new SearchTerms(symptom.trim)
370+
val cleanedUpAbbrev = mmAbbrev.replaceAll("[^0-9A-Za-z\\-]", "")
371+
372+
// Check if user is allowed to access the resource at all (might be Private or Protected and user not logged in)
373+
if (mmDao.getMMsAndRemedies(getAuthenticatedUser(request)).find(_.mminfo.abbrev == cleanedUpAbbrev) == None) {
374+
Logger.info(s"Get: apiLookupMM(abbrev: ${mmAbbrev}, symptom: ${symptom}, page: ${page}, remedy: ${remedyString}): user not allowed to access ressource.")
331375
NoContent
376+
}
377+
else {
378+
// Do actual look-up and return results in case of success.
379+
mmDao.getSectionHits(cleanedUpAbbrev, searchTerms, page, Some(remedyString)) match {
380+
case Some(sectionHits) if (sectionHits.results.length > 0 || sectionHits.numberOfMatchingSectionsPerChapter.length > 0) =>
381+
Ok(sectionHits.asJson.toString())
382+
case _ =>
383+
Logger.info(s"Get: apiLookupMM(abbrev: ${mmAbbrev}, symptom: ${symptom}, page: ${page}, remedy: ${remedyString}): no results found")
384+
NoContent
385+
}
386+
}
332387
}
333388
}
334389
}
335-
}
336390

337391
}
338392

backend/app/org/multics/baueran/frep/backend/controllers/package.scala

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package org.multics.baueran.frep.backend
22

33
import play.api.mvc._
44
import org.multics.baueran.frep.backend.dao.{CazeDao, EmailHistoryDao, FileDao, MMDao, MemberDao, PasswordChangeRequestDao, RepertoryDao}
5+
import org.multics.baueran.frep.shared.Defs.{CookieFields, HeaderFields}
56
import org.multics.baueran.frep.shared.Member
67

78
package object controllers {

backend/conf/application.conf

+39
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,50 @@
77
# 'null' means to not set this header at all here.
88
play.filters.headers.contentSecurityPolicy = null
99

10+
play.modules.enabled += "play.filters.csrf.CSRFModule"
1011
play.filters.enabled += "play.filters.csrf.CSRFFilter"
1112

1213
play.filters.csrf.cookie.name = "csrfCookie"
1314
play.filters.csrf.token.name = "csrfToken"
1415
play.filters.csrf.cookie.secure = true
16+
17+
# play.filters.csrf {
18+
# bypassCorsTrustedOrigins = false
19+
20+
# method {
21+
# # If non empty, then requests will be checked if the method is not in this list.
22+
# # whiteList = ["GET", "HEAD", "OPTIONS"]
23+
# whitelist = []
24+
25+
# # The black list is only used if the white list is empty.
26+
# # Only check methods in this list.
27+
# blackList = [ "GET", "PUT", "POST", "DELETE" ]
28+
# }
29+
30+
# header {
31+
# # The name of the header to accept CSRF tokens from.
32+
# # name = "Csrf-Token"
33+
# name = "csrfCookie"
34+
35+
# # Defines headers that must be present to perform the CSRF check. If any of these headers are present, the CSRF
36+
# # check will be performed.
37+
# #
38+
# # By default, we only perform the CSRF check if there are Cookies or an Authorization header.
39+
# # Generally, CSRF attacks use a user's browser to execute requests on the client's behalf. If the user does not
40+
# # have an active session, there is no danger of this happening.
41+
# #
42+
# # Setting this to null or an empty object will protect all requests.
43+
# protectHeaders {
44+
# Cookie = "*"
45+
# Authorization = "*"
46+
# }
47+
48+
# # Defines headers that can be used to bypass the CSRF check if any are present. A value of "*" simply
49+
# # checks for the presence of the header. A string value checks for a match on that string.
50+
# bypassHeaders {}
51+
# }
52+
# }
53+
1554
play.http.session.secure = true
1655
play.http.session.httpOnly = false
1756
play.http.session.sameSite = "strict"

build.sbt

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import sbt.Keys.libraryDependencies
22

3-
val myScalaVersion = "2.13.12"
3+
val myScalaVersion = "2.13.13"
44
val scalaTestPlusVersion = "5.1.0"
55
val scalaJsDomVersion = "2.3.0"
66
val scalaTagsVersion = "0.12.0"
@@ -98,7 +98,7 @@ lazy val commonSettings = Seq(
9898
scalaVersion := myScalaVersion,
9999
organization := "org.multics.baueran.frep",
100100
maintainer := "[email protected]",
101-
version := "0.16.0"
101+
version := "0.16.1"
102102
)
103103

104104
// loads the frontend project at sbt startup

docker/.env

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
VERSION=0.16.0
1+
VERSION=0.16.1

project/plugins.sbt

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ resolvers += "Typesafe repository" at "https://repo.typesafe.com/typesafe/releas
77

88
// Sbt plugins
99
addSbtPlugin("com.vmunier" % "sbt-web-scalajs" % "1.3.0")
10-
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.15.0")
10+
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.16.0")
1111
addSbtPlugin("org.scala-js" % "sbt-jsdependencies" % "1.0.2")
1212

1313
addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.3.1")
1414
addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.3.1")
1515
addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.14")
1616

17-
addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.9.0")
17+
addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.9.2")
1818
addSbtPlugin("com.typesafe.sbt" % "sbt-gzip" % "1.0.2")
1919
addSbtPlugin("com.typesafe.sbt" % "sbt-digest" % "1.1.4")

shared/src/main/scala/org/multics/baueran/frep/shared/Defs/package.scala

+5
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,9 @@ package object Defs {
2323
val id, csrfCookie, cookiePopupAccepted, theme = Value
2424
}
2525

26+
object HeaderFields extends Enumeration {
27+
type HeaderFields = Value
28+
val csrfToken = "Csrf-Token"
29+
}
30+
2631
}

0 commit comments

Comments
 (0)