HackToTech

Hack To Technology

CVE-2022-45143周りを見たのでメモ

https://lists.apache.org/thread/yqkd183xrw3wqvnpcg3osbcryq85fkzjjvn.jp

仕事していて話題にあがったので暇つぶしがてら見てみた

とりあえず素のtomcatで試す

conf/server.xmlを書き換える(関係ない箇所は省略)

<Server port="8005" shutdown="SHUTDOWN">
  <Service name="Catalina">
    <Engine name="Catalina" defaultHost="localhost">
      <Host name="localhost"  appBase="webapps" unpackWARs="true" autoDeploy="true" errorReportValveClass="org.apache.catalina.valves.JsonErrorReportValve">
      </Host>
    </Engine>
  </Service>
</Server>

HostタグでerrorReportValveClassを指定することで切り替えできる(デフォルトは ErrorReportValve が使用される)

10.1.2で修正されているらしいので、 その辺りの挙動を一応見る(Commit)

apache-tomcat-10.1.1

curl http://localhost:8080/\"\"/ -H 'accept:application/json'

そのまま出力しようとするせいでJSONが壊れる

{
  "type": "Exception Report",
  "message": "Invalid character found in the request target [/""/ ]. The valid characters are defined in RFC 7230 and RFC 3986",
  "description": "The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing)."
}

apache-tomcat-10.1.4

curl http://localhost:8080/\"\"/ -H 'accept:application/json'

10.1系の最新であればちゃんとエスケープされる

{
  "type": "Exception Report",
  "message": "Invalid character found in the request target [/\u0022\u0022/ ]. The valid characters are defined in RFC 7230 and RFC 3986",
  "description": "The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing)."
}

spring-bootの埋め込みtomcatで試す

spring-bootの埋め込みtomcatの errorReportValveClass をどうやって書き換えるのか調べていたら、先にわかりやすく書いてくださっている方がいたのでそれをKotlinで書いて試した
参考にさせていただいたページ: Spring BootでTomcatのデフォルトエラーページが出るのを抑止する

package com.example.app.presentation.configuration

import org.apache.catalina.Context
import org.apache.catalina.core.StandardHost
import org.springframework.boot.web.embedded.tomcat.TomcatContextCustomizer
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory
import org.springframework.boot.web.server.WebServerFactoryCustomizer
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.apache.catalina.valves.JsonErrorReportValve


@Configuration
class TomcatConfig {
    @Bean
    fun errorReportValveCustomizer(): WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
        return WebServerFactoryCustomizer { factory: TomcatServletWebServerFactory ->
            factory.addContextCustomizers(TomcatContextCustomizer { context: Context ->
                if (context.parent is StandardHost) {
                    (context.parent as StandardHost).errorReportValveClass = JsonErrorReportValve::class.qualifiedName
                }
            })
        }
    }
}

spring-boot-starter-web-2.7.5 + tomcat-embed-core-9.0.68

ちょっと他のプロジェクトでそのまま試していたのでstarterとかが古いが気にしない

curl http://localhost:8080/\"\"/ -H 'accept:application/json'

そのまま出力しようとするせいでJSONが壊れる

{
  "type": "Exception Report",
  "message": "Invalid character found in the request target [/""/ ]. The valid characters are defined in RFC 7230 and RFC 3986",
  "description": "The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing)."
}

spring-boot-starter-web-2.7.5 + tomcat-embed-core-9.0.69

9系は9.0.69で修正されている

curl http://localhost:8080/\"\"/ -H 'accept: application/json'
{
  "type": "Exception Report",
  "message": "Invalid character found in the request target [/\u0022\u0022/ ]. The valid characters are defined in RFC 7230 and RFC 3986",
  "description": "The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing)."
}

まあデフォルトをそのまま使っていれば遭遇することはなさそうだし、バージョンあげれば修正されるしで気にしなくて良さそう感

Spring Bootのエラーページ周りを色々見たのでメモ

Spring bootを仕事で使っていて、
server.error.whitelabel.enabled を指定すると具体的にどうなるのかとか、
誰がどうレスポンスを生成しているのかとか、
この辺イマイチ浅い理解をしていてちゃんと説明出来そうにないのでちゃんとデバッガを動かしながら見てみた

読んでいたところのコードの各種バージョン

  • SpringBoot(2.7.6)
  • Tomcat(10.1.4)
  • SpringWebMvc(5.3.24)

理解する上で併せて読むと良い公式ブログ spring.io

server.error.whitelabel.enabled

trueを指定した場合(デフォルト)

WhitelabelErrorViewConfigurationが有効になる(defaultErrorViewのBeanが登録される)

ErrorMvcAutoConfiguration#WhitelabelErrorViewConfiguration

この状態でaccept: text/htmlのリクエストを投げた場合
Viewは↓で生成されたものが返される

ErrorMvcAutoConfiguration#StaticView

<html><body><h1>Whitelabel Error Page</h1><p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p><div id='created'>Sat Dec 10 16:54:07 JST 2022</div><div>There was an unexpected error (type=Not Found, status=404).</div></body></html>

この状態でapplication/jsonのリクエストを投げた場合

{"timestamp":"2022-12-10T07:59:51.927+00:00","status":404,"error":"Not Found","path":"/base/test"}

falseを指定した場合

この状態でaccept: text/htmlのリクエストを投げた場合
↓でTomcat側で生成されたものが返される(Tomcatを使っている場合) ErrorReportValve#report

<!doctype html><html lang="en"><head><title>HTTP Status 404 – Not Found</title><style type="text/css">body {font-family:Tahoma,Arial,sans-serif;} h1, h2, h3, b {color:white;background-color:#525D76;} h1 {font-size:22px;} h2 {font-size:16px;} h3 {font-size:14px;} p {font-size:12px;} a {color:black;} .line {height:1px;background-color:#525D76;border:none;}</style></head><body><h1>HTTP Status 404 – Not Found</h1></body></html>

この状態でapplication/jsonのリクエストを投げた場合
trueの時と同じものが返される

{"timestamp":"2022-12-10T07:57:47.967+00:00","status":404,"error":"Not Found","path":"/base/test"}

上の部分のレスポンス返すまでどういう挙動をしているのかが気になったので、ErrorController周りを見ることにした

ErrorController周り

ErrorControllerBean が無ければ BasicErrorController が登録される ErrorMvcAutoConfiguration#basicErrorController

ErrorControllerのインタフェース

ErrorControllerを実装した抽象クラスのAbstractErrorController

AbstractErrorControllerを継承したBasicErrorController

BasicErrorControllerの中身

htmlとその他のAcceptHeaderで呼び分けているところ
AcceptHeadertext/htmlがあるならBasicErrorController#errorHTML

それ以外の場合はBasicErrorController#error

HttpMediaTypeNotAcceptableExceptionが起きた場合にエラーステータスをそのまま返すだけのExceptionHandlerも登録している

大体何をやっているかがわかったので、ErrorControllerを実装してカスタムなErrorControllerを書いてみた

ErrorControllerを実装したカスタムなErrorController

実装自体は少し BasicErrorController をコピーしつつjavaからKotlinに直している
errorのメソッドは特に書くことはなくて、
mediaTypeNotAcceptableのメソッドの方は、ResponseEntity<Any>で返すと最終的にDefaultHandlerExceptionResolverに捕まってレスポンスボディがnullになるので(後述)、ObjectMapper使ってResponseEntity<String>で返させている
そして、仮にval status = getStatus(request)statusを受け取って渡さなかった場合、HttpMediaTypeNotAcceptableのレスポンスコードとして用意されている406が返される
text/htmlだろうがなんだろうがJSON的なレスポンスを強制的に返したかったのでやっているが、本来は406を返しつつ受け取れるメディアタイプを返す実装の方が正しいとは思う
(ただまあ404なエンドポイントに投げられて、統一したレスポンスボディ返しつつ406ではなく404を返したいケースはある気がする)

import com.fasterxml.jackson.databind.ObjectMapper
import org.springframework.boot.web.servlet.error.ErrorController
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.HttpMediaTypeNotAcceptableException
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import javax.servlet.RequestDispatcher
import javax.servlet.http.HttpServletRequest

@RestController
class ErrorController(
    private val objectMapper: ObjectMapper,
): ErrorController {

    @RequestMapping("/error")
    fun error(request: HttpServletRequest): ResponseEntity<Any> {
        val status = getStatus(request)
        val body = mapOf("message" to status.reasonPhrase, "status" to status.value())
        return ResponseEntity(body, status)
    }

    @ExceptionHandler(HttpMediaTypeNotAcceptableException::class)
    fun mediaTypeNotAcceptable(request: HttpServletRequest): ResponseEntity<String> {
        val status = getStatus(request)
        val body = mapOf("message" to status.reasonPhrase, "status" to status.value())
        return ResponseEntity(objectMapper.writeValueAsString(body), status)
    }

    private fun getStatus(request: HttpServletRequest): HttpStatus {
        val statusCode = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE) as Int? ?: return HttpStatus.INTERNAL_SERVER_ERROR
        return try {
            HttpStatus.valueOf(statusCode)
        } catch (ex: Exception) {
            HttpStatus.INTERNAL_SERVER_ERROR
        }
    }
}

accept application/json

curl -XGET http://localhost:8080/404 -H 'accept: application/json' -sS -v | jq
*   Trying 127.0.0.1:8080...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /404 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.81.0
> accept: application/json
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 404 
< Vary: Origin
< Vary: Access-Control-Request-Method
< Vary: Access-Control-Request-Headers
< Content-Type: application/json
< Transfer-Encoding: chunked
< Date: Sat, 10 Dec 2022 16:39:19 GMT
< 
{ [47 bytes data]
* Connection #0 to host localhost left intact
{
  "message": "Not Found",
  "status": 404
}

accept text/html

curl -XGET http://localhost:8080/404 -H 'accept: text/html' -sS -v | jq
*   Trying 127.0.0.1:8080...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /404 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.81.0
> accept: text/html
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 404 
< Vary: Origin
< Vary: Access-Control-Request-Method
< Vary: Access-Control-Request-Headers
< Content-Type: text/html;charset=UTF-8
< Content-Length: 36
< Date: Sat, 10 Dec 2022 16:39:42 GMT
< 
{ [36 bytes data]
* Connection #0 to host localhost left intact
{
  "message": "Not Found",
  "status": 404
}

accept: text/html の場合にResponseEntity<Any>を返しても最終的にDefaultHandlerExceptionResolverに捕まってResponseのBodynullのステータス406で返ってくる部分

上のErrorControllerでエラーレスポンスを返した際に、 AbstractMessageConverterMethodProcessor#writeWithMessageConverters 辺りでmediaTypesapplication/json, application/*+jsonにマッチしない且つbodynullではないので、
HttpMediaTypeNotAcceptableException投げられることになる

で、その後にDefaultHandlerExceptionResolver側で捕捉されて、406,nullで返されることになる
DefaultHandlerExceptionResolver#doResolveException DefaultHandlerExceptionResolver#handleHttpMediaTypeNotAcceptable

実際にFramework側のコードを読みながら手を動かすと理解が進むので今後も暇を見つけてやっていきたい

test-retry-gradle-pluginで特定のJunit5のNested ClassのRetryを実行させる

公式に書いてなくて探してすぐに見当たらなかったので備忘録

github.com

Nested Classの指定は . ではなく $ でのアクセスになる

# build.gradle.kts
plugins {
    id("org.gradle.test-retry") version "1.4.1"
}
tasks.withType<Test> {
    useJUnitPlatform()

    retry {
        maxRetries.set(3)
        filter {
            includeClasses.add("com.example.app.Test\$InnerClassA")
        }
    }
}

もし配下のNested Classすべてを実行するなら includeClasses.add("com.example.app.Test\$*") とかにする

# Test code
package com.example.app

import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.fail

class Test {
    @Nested
    inner class InnerClassA {
        @Test
        fun `リトライされること`() {
            fail("retry")
        }
    }

    @Nested
    inner class InnerClassB {
        @Test
        fun `リトライされないこと`() {
            fail("not retry")
        }
    }
}

実行結果

$ ./gradlew test

> Task :test

Test > InnerClassA > リトライされること() FAILED
    org.opentest4j.AssertionFailedError at Test.kt:12

Test > InnerClassB > リトライされないこと() FAILED
    org.opentest4j.AssertionFailedError at Test.kt:20

Test > InnerClassA > リトライされること() FAILED
    org.opentest4j.AssertionFailedError at Test.kt:12

Test > InnerClassA > リトライされること() FAILED
    org.opentest4j.AssertionFailedError at Test.kt:12

Test > InnerClassA > リトライされること() FAILED
    org.opentest4j.AssertionFailedError at Test.kt:12

5 tests completed, 5 failed

> Task :test FAILED

FAILURE: Build failed with an exception.

BUILD FAILED in 6s
6 actionable tasks: 2 executed, 4 up-to-date

英語環境のUbuntuで日本語のGoogle Chromeを使う

備忘録として書いておく

環境: Ubuntu 22.04.1 LTS

Windowsではブラウザ側で指定できるくせにLinuxだとシステムのデフォルト言語しか使えないらしい
Chrome の言語の変更とウェブページの翻訳 - パソコン - Google Chrome ヘルプ

$ LANGUAGE=ja_JP google-chrome

で起動すれば日本語として起動できるので、これで起動することにした
Ubuntuのランチャーから押しても出来るようにしたかったので、適当に.desktopファイル作って起動後にfavoriteしている

cat << EOF > ~/.local/share/applications/chrome-ja.desktop
[Desktop Entry]
Name=Google Chrome Ja
Exec=env LANGUAGE=ja_JP google-chrome
Type=Application
Icon=google-chrome
EOF

KotlinのCompanion ObjectでGenericsを使った関数を定義したい

個人的な備忘録
IDのクラスに対してGenericsを使った共通の関数を持たせたいなーというのがあって、
reflection使えばとりあえず出来そうだったのでこれを仕事で使ってみている
ただなんかcompanion object側のクラスに対してinterface継承させずにやる方法もあるんじゃないかと思って若干もやっている
(調べて見た感じぱっと見当たらなかった)