[インデックス 10011] reflect: disallow Interface method on Value obtained via unexported name
コミット
コミットハッシュ: 304cf4dc9b6c289d4e458872d83d8f409ab72c07
作成者: Russ Cox rsc@golang.org
日付: 2011年10月17日 18:48:45 -0400
メッセージ: reflect: disallow Interface method on Value obtained via unexported name
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/304cf4dc9b6c289d4e458872d83d8f409ab72c07
元コミット内容
reflect: disallow Interface method on Value obtained via unexported name
Had been allowing it for use by fmt, but it is too hard to lock down.
Fix other packages not to depend on it.
R=r, r
CC=golang-dev
https://golang.org/cl/5266054
変更されたファイル:
- src/pkg/fmt/fmt_test.go (16行変更)
- src/pkg/fmt/print.go (247行変更)
- src/pkg/reflect/all_test.go (115行変更)
- src/pkg/reflect/deepequal.go (2行変更)
- src/pkg/reflect/value.go (31行変更)
- test/interface/fake.go (40行変更)
全体で286行追加、165行削除。
変更の背景
このコミットは、Go言語の初期段階(2011年)において、リフレクション(reflection)機能におけるセキュリティとカプセル化の強化を目的として実装されました。
問題の発端
Go言語の初期実装では、reflect.Value.Interface()
メソッドが、非公開(unexported)フィールドから取得した値に対しても動作していました。これは主にfmt
パッケージの内部処理を支援するために許可されていましたが、この仕様はセキュリティ上の重大な問題を引き起こしていました。
セキュリティ上の懸念
非公開フィールドへのアクセスを許可することで、以下の問題が発生していました:
- カプセル化の破綻: Goの型システムが提供するアクセス制御が無効化される
- データ整合性の問題: 非公開フィールドが意図しない方法で外部から操作される可能性
- セキュリティホール: 機密情報や内部状態への意図しないアクセス
決定的な要因
コミットメッセージの「it is too hard to lock down」(制限するには複雑すぎる)という記述から、Go開発チームは部分的な制限を実装するよりも、完全に禁止する方が安全であると判断しました。
前提知識の解説
Go言語におけるリフレクション
リフレクションは、プログラムが実行時に自身の構造を調べたり変更したりする機能です。Go言語ではreflect
パッケージがこの機能を提供します。
// 基本的なリフレクションの使用例
type Person struct {
Name string // 公開フィールド
age int // 非公開フィールド
}
p := Person{Name: "Alice", age: 30}
v := reflect.ValueOf(p)
// 公開フィールドへのアクセス
nameValue := v.FieldByName("Name")
fmt.Println(nameValue.Interface()) // "Alice"
// 非公開フィールドへのアクセス(このコミット以降はpanicする)
ageValue := v.FieldByName("age")
// ageValue.Interface() // panic: cannot return value obtained from unexported field
公開・非公開の識別子
Go言語では、識別子(変数名、関数名、型名など)の最初の文字が大文字の場合は公開(exported)、小文字の場合は非公開(unexported)として扱われます。
type Example struct {
PublicField string // 公開フィールド
privateField string // 非公開フィールド
}
func (e Example) PublicMethod() {} // 公開メソッド
func (e Example) privateMethod() {} // 非公開メソッド
fmt パッケージの内部動作
fmt
パッケージは、値を文字列に変換する際にリフレクションを多用します。特に構造体の内容を表示する場合、各フィールドの値を取得するためにreflect.Value.Interface()
を使用していました。
技術的詳細
主要な変更点
1. reflect.Value.Interface() の制限強化
変更前:
func (v Value) Interface() interface{} {
return v.internal().Interface()
}
変更後:
func (v Value) Interface() interface{} {
return valueInterface(v, true)
}
func valueInterface(v Value, safe bool) interface{} {
iv := v.internal()
return iv.valueInterface(safe)
}
func (iv internalValue) valueInterface(safe bool) interface{} {
// ... 省略 ...
if safe && iv.flag&flagRO != 0 {
// Do not allow access to unexported values via Interface,
// because they might be pointers that should not be
// writable or methods or function that should not be callable.
panic("reflect.Value.Interface: cannot return value obtained from unexported field or method")
}
// ... 省略 ...
}
2. fmt パッケージの大幅な書き換え
fmtパッケージは、非公開フィールドに依存しないように完全に書き換えられました。主な変更点:
printField()
関数からprintValue()
関数への分離printReflectValue()
関数の新設handleMethods()
関数の追加による、カスタムフォーマッタの処理分離
3. CanInterface() メソッドの強化
func (v Value) CanInterface() bool {
if iv.kind == Invalid {
panic(&ValueError{"reflect.Value.CanInterface", iv.kind})
}
return v.InternalMethod == 0 && iv.flag&flagRO == 0
}
flagRO
(read-only flag)の検査を追加し、非公開フィールドかどうかを判定するようになりました。
4. エラーハンドリングの改善
func (p *pp) badVerb(verb int, val interface{}, val1 reflect.Value) {
p.add('%')
p.add('!')
p.add(verb)
p.add('(')
switch {
case val != nil:
p.buf.WriteString(reflect.TypeOf(val).String())
p.add('=')
p.printField(val, 'v', false, false, 0)
case val1.IsValid():
p.buf.WriteString(val1.Type().String())
p.add('=')
p.printValue(val1, 'v', false, false, 0)
default:
p.buf.Write(nilAngleBytes)
}
p.add(')')
}
エラーメッセージの生成において、reflect.Value
も適切に処理できるように改善されました。
フラグベースの制御機構
このコミットでは、flagRO
(read-only)フラグを使用して非公開フィールドへのアクセスを制御しています。
const (
flagRO flag = 1 << iota
// その他のフラグ
)
非公開フィールドから取得されたreflect.Value
にはflagRO
フラグが設定され、Interface()
メソッドはこのフラグを検査してアクセスを拒否します。
コアとなるコードの変更箇所
1. reflect/value.go における主要変更
// 新しい valueInterface 関数
func valueInterface(v Value, safe bool) interface{} {
iv := v.internal()
return iv.valueInterface(safe)
}
func (iv internalValue) valueInterface(safe bool) interface{} {
if iv.kind == 0 {
panic(&ValueError{"reflect.Value.Interface", iv.kind})
}
if iv.method {
panic("reflect.Value.Interface: cannot create interface value for method with bound receiver")
}
if safe && iv.flag&flagRO != 0 {
// Do not allow access to unexported values via Interface,
// because they might be pointers that should not be
// writable or methods or function that should not be callable.
panic("reflect.Value.Interface: cannot return value obtained from unexported field or method")
}
// ... 続く
}
2. fmt/print.go における構造的変更
// 新しい printValue 関数
func (p *pp) printValue(value reflect.Value, verb int, plus, goSyntax bool, depth int) (wasString bool) {
if !value.IsValid() {
if verb == 'T' || verb == 'v' {
p.buf.Write(nilAngleBytes)
} else {
p.badVerb(verb, nil, value)
}
return false
}
// Handle values with special methods.
var field interface{}
if value.CanInterface() {
field = value.Interface()
}
if wasString, handled := p.handleMethods(field, verb, plus, goSyntax, depth); handled {
return wasString
}
return p.printReflectValue(value, verb, plus, goSyntax, depth)
}
3. テストケースの更新
非公開フィールドを公開フィールドに変更:
// 変更前
type B struct {
i I
j int
}
// 変更後
type B struct {
I I
j int
}
コアとなるコードの解説
セキュリティ制御の実装
このコミットの最も重要な変更は、flagRO
フラグを使用したアクセス制御の実装です。
if safe && iv.flag&flagRO != 0 {
panic("reflect.Value.Interface: cannot return value obtained from unexported field or method")
}
この単純な検査により、非公開フィールドへのアクセスが完全に遮断されます。safe
パラメータは、内部的な安全なアクセス(例:reflect.DeepEqual
)を許可するために使用されます。
fmt パッケージの新しいアーキテクチャ
fmt パッケージは、非公開フィールドに依存しない新しいアーキテクチャに変更されました:
- printField():
interface{}
値から開始する関数 - printValue():
reflect.Value
から開始する関数 - printReflectValue(): 低レベルなリフレクション処理を担当する関数
この分離により、各レベルで適切なセキュリティ制御が可能になりました。
getField() 関数の改善
func getField(v reflect.Value, i int) reflect.Value {
val := v.Field(i)
if val.Kind() == reflect.Interface && !val.IsNil() {
val = val.Elem()
}
return val
}
インターフェース型のフィールドを適切に処理するために、val.Elem()
を使用して実際の値を取得するよう改善されました。
メソッドハンドリングの分離
func (p *pp) handleMethods(field interface{}, verb int, plus, goSyntax bool, depth int) (wasString, handled bool) {
// Formatter インターフェースの処理
if formatter, ok := field.(Formatter); ok {
handled = true
wasString = false
defer p.catchPanic(field, verb)
formatter.Format(p, verb)
return
}
// GoStringer インターフェースの処理
if goSyntax {
p.fmt.sharp = false
if stringer, ok := field.(GoStringer); ok {
wasString = false
handled = true
defer p.catchPanic(field, verb)
p.fmtString(stringer.GoString(), 's', false, field, reflect.Value{})
return
}
}
// 通常の Stringer インターフェースの処理
if stringer, ok := field.(Stringer); ok {
wasString = false
handled = true
defer p.catchPanic(field, verb)
p.printField(stringer.String(), verb, plus, false, depth)
return
}
handled = false
return
}
カスタムフォーマッタの処理を分離することで、セキュリティ制御を適切に実装できました。
関連リンク
参考にした情報源リンク
- Go Forum - reflect.Value.Interface panic エラーについて
- GitHub Issue #19752 - reflect: support accessing unexported fields
- GitHub Issue #8965 - fmt: support printing reflect.Value directly
- Stack Overflow - How to access unexported struct fields
- Adventures in Go: Accessing Unexported Functions - Alan Pierce