Skip to content

Commit f385169

Browse files
oslfmtsmoelius
authored andcommitted
add descriptions for all type-cosplay examples
1 parent 9f95c1f commit f385169

File tree

6 files changed

+64
-22
lines changed

6 files changed

+64
-22
lines changed

lints/type_cosplay/README.md

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,72 @@ or u8, and not an enum. This will flag a false positive.
3434

3535
## Note on Tests
3636

37-
**insecure-anchor**: insecure because `User` type derives Discriminator trait (via `#[account]`),
37+
### insecure
38+
39+
This is the canonical example of type-cosplay. The program tries to deserialize
40+
bytes from `AccountInfo.data` into the `User` type. However, a malicious user could pass in
41+
an account that has in it's data field the `Metadata` type. This type is equivalent to the
42+
`User` type, and the data bytes will thus successfully deserialize as a `User` type. The
43+
program performs no checks whatsoever, and will continue on operating with a pubkey that it
44+
believes to be a `User` pubkey, not a `Metadata` pubkey.
45+
46+
### insecure-2
47+
48+
This is insecure because the program tries to deserialize from multiple enum types.
49+
Here, `UserInfo` and `MetadataInfo` enums are both being deserialized. Note that both of these
50+
enums contain a single variant, with the struct type nested inside it. This evades the in-built
51+
discriminant of an enum. A `Metadata` type could be deserialized into a `UserInfo::User(User)`,
52+
and a `User` could be deserialized into a `MetadataInfo::Metadata(Metadata)`.
53+
54+
Only deserializing from a single enum is safe since enums contain a natural, in-built discriminator.
55+
If _all_ types are nested under a variant of this enum, then when deserializing, the enum variant
56+
must be matched first, thus guaranteeing differentiation between types.
57+
58+
However, deserializing from multiple enums partitions the "set of types" and is thus not exhaustive
59+
in discriminating between all types. If multiple enums are used to encompass the types, there may
60+
be two equivalent types that are variants under different enums, as seen in this example.
61+
62+
### insecure-3
63+
64+
This example is insecure because `AccountWithDiscriminant` could be deserialized as a
65+
`User`, if the variant is `Extra(Extra)`. The first byte would be 0, to indicate the discriminant
66+
in both cases, and the next 32 bytes would be the pubkey. The problem here is similar to
67+
the insecure-2 example--not all types are nested under a single enum type. Except here,
68+
instead of using another enum, the program also tries to deserialize `User`.
69+
70+
This illustrates that in order to properly take advantage of the enums natural built-in
71+
discriminator, you must nest _all_ types in your program as variants of this enum, and
72+
only serialize and deserialize this enum type.
73+
74+
### insecure-anchor
75+
76+
Insecure because `User` type derives Discriminator trait (via `#[account]`),
3877
thus one may expect this code to be secure. However, the program tries to deserialize with
3978
`try_from_slice`, the default borsh deserialization method, which does _not_ check for the
4079
discriminator. Thus, one could potentially serialize a `Metadata` struct, and then later
4180
deserialize without any problem into a `User` struct, leading to a type-cosplay vulnerability.
4281

43-
**recommended**: this is secure code because all structs have an `#[account]` macro attributed
82+
### recommended
83+
84+
This is secure code because all structs have an `#[account]` macro attributed
4485
on them, thus deriving the `Discriminator` trait for each. Further, unlike the insecure-anchor
4586
example, the program uses the proper deserialization method, `try_deserialize`, to deserialize
4687
bytes as `User`. This is "proper" because in the derived implementation of `try_deserialize`,
4788
the discriminator of the type is checked first.
89+
90+
_Note: this example differs from the Sealevel [recommended](https://github.com/coral-xyz/sealevel-attacks/blob/master/programs/3-type-cosplay/recommended/src/lib.rs) example in that it actually attempts_
91+
_to perform a deserialization in the function body, and then uses the struct. It provides_
92+
_a more realistic and concrete example of what might happen in real programs_
93+
94+
### secure
95+
96+
This example is from the Sealevel [example](https://github.com/coral-xyz/sealevel-attacks/blob/master/programs/3-type-cosplay/secure/src/lib.rs). It fixes the insecure case by adding a `discriminant`
97+
field to each struct, and further this discriminant is "proper" because it contains the
98+
necessary amount of variants in order to differentiate each type. In the code, there is
99+
an explicit check to make sure the discriminant is as expected.
100+
101+
### secure-2
102+
103+
This example fixes both the insecure and insecure-2 examples. It is secure because it only deserializes
104+
from a single enum, and that enum encapsulates all of the user-defined types. Since enums contain
105+
an implicit discriminant, this program will always be secure as long as all types are defined under the enum.

lints/type_cosplay/ui/insecure-2/src/lib.rs

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,6 @@ use borsh::{BorshDeserialize, BorshSerialize};
33

44
declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
55

6-
// NOTE: this is insecure because the program tries to deserialize from multiple enum types.
7-
// This is a vulnerability because it partitions the "set of types" and is thus not exhaustive
8-
// in discriminating between all types. Deserializing from a single enum is safe because the
9-
// program will have to match the deserialized result against an enum variant in order to reconstruct
10-
// the ADT. This effectively serves as a discriminant, because even if two types deserialize the same,
11-
// the programmer will explicitly match it to a type.
12-
// If multiple enums are used to encompass the types, this defense will basically be breached.
13-
// There may be two different types that are equivalent. If we deserialize from an enum,
14-
// type B could be passed in, when what was expected was type A.
15-
166
// NOTE: what about the case where we only deserialize from one enum? Could this still be a vulnerability?
177
// ex. only deser from UserInfo. Someone sets data as MetadataInfo::Metadata(...). will this match?
188
// what if MetadataInfo::User(...)?

lints/type_cosplay/ui/insecure-2/src/lib.stderr

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
error: multiple enum types deserialized. Should only have one enum type to avoid possible equivalent types
2-
--> $DIR/lib.rs:27:20
2+
--> $DIR/lib.rs:17:20
33
|
44
LL | let user = UserInfo::try_from_slice(&ctx.accounts.user.data.borrow()).unwrap();
55
| ^^^^^^^^
66
|
77
= note: `-D type-cosplay` implied by `-D warnings`
88
help: consider constructing a single enum that contains all type definitions as variants
9-
--> $DIR/lib.rs:41:24
9+
--> $DIR/lib.rs:31:24
1010
|
1111
LL | let metadata = MetadataInfo::try_from_slice(&ctx.accounts.user.data.borrow()).unwrap();
1212
| ^^^^^^^^^^^^

lints/type_cosplay/ui/insecure-3/src/lib.rs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,6 @@ use borsh::{BorshDeserialize, BorshSerialize};
33

44
declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
55

6-
// This example is insecure because AccountWithDiscriminant could be deserialized as a
7-
// User, if the variant is Extra(Extra). The first byte would be 0, to indicate the discriminant
8-
// in both cases, and the next 32 bytes would be the pubkey.
96
#[program]
107
pub mod type_cosplay_secure {
118
use super::*;

lints/type_cosplay/ui/insecure-3/src/lib.stderr

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
error: Deserializing from different ADT types.
2-
--> $DIR/lib.rs:16:20
2+
--> $DIR/lib.rs:13:20
33
|
44
LL | let user = User::try_from_slice(&ctx.accounts.user.data.borrow()).unwrap();
55
| ^^^^
66
|
77
= note: `-D type-cosplay` implied by `-D warnings`
88
help: deserialize from only structs with a discriminant, or an enum encapsulating all structs.
9-
--> $DIR/lib.rs:29:13
9+
--> $DIR/lib.rs:26:13
1010
|
1111
LL | AccountWithDiscriminant::try_from_slice(&ctx.accounts.user.data.borrow()).unwrap();
1212
| ^^^^^^^^^^^^^^^^^^^^^^^

lints/type_cosplay/ui/secure-2/src/lib.rs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,6 @@ use borsh::{BorshDeserialize, BorshSerialize};
33

44
declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
55

6-
// This example is secure because it only deserializes from a single enum, and that enum
7-
// encapsulates all of the user-defined types. Since enums contain an implicit discriminant,
8-
// this program will always be secure as long as all types are defined under the enum.
96
#[program]
107
pub mod type_cosplay_secure {
118
use super::*;

0 commit comments

Comments
 (0)