ORM: exploiting cascades with improper input validation

In 2021, the OWASP top 10, which highlights the most common vulnerabilities in applications, has slightly changed. Injection vulnerabilities, previously the most critical, are now in third place.

One reason for this is that developers are becoming more aware of the risks associated with injection vulnerabilities through the implementation of more secure application development tools and practices. And of course, the most important measure to mitigate the risk of SQL injection is the use of prepared statements.

This is usually done using an ORM, which can introduce new risks as we will see in this article.

What is an ORM?

Most applications need to manipulate and store data. To do this and to make their work easier, developers can use ORMs (Object-Relational Mapping), which make it possible to manage access to a database in a secure manner (in theory).

Indeed, an ORM consists of a set of classes and methods allowing the tables of a relational database to be manipulated as objects.

Here, we will look at TypeORM, an ORM for TypeScript and JavaScript. The developer will then write models that can be related to each other. We are interested in the properties of the relations between two models, in particular those of cascade.

If this property is set as true between two entities, then updating one side of the relation and saving on the other will update both sides in the database and may even create new objects.

However, as noted in the documentation for cascades, they may seem like a simple and efficient way to work with relations, but they can also cause bugs and security problems. In addition, they are a less explicit way of registering new objects in the database.

However, this does not prevent developers from using them. It must be said that it is quite practical when there is an update of several related entities to have to save only one to save the others.

Scenario of exploiting cascades with improper input validation

Let’s look at a concrete example, inspired by a case encountered during a web application penetration test.

It is common for applications to split the management of each resource and implement security tests in the corresponding place.

One can therefore imagine an application with users and associated documents.

ts 
@Entity()
class User {
        @PrimaryGeneratedColumn()
        id!: number

        @Column()
        name!: string

        @Column({ default: false })
        admin!: boolean
}

@Entity() 
class Document {
        @PrimaryGeneratedColumn()
        id!: number

        @Column()
        content!: String

        @Column()
        authorId!: number

        @OneToOne(() => User, { cascade: true })
        @JoinColumn()
        author!: User
}

The two resources are linked through the OneToOne relation on Document which has the particularity of being in cascade.

It is now possible to imagine each resource having its own controller. For example, the one for users could allow any resource to be read, but limit the update of protected fields (such as roles) or simply not allow a user to be updated.

ts
app.get("/user", async (_request, reply) => {
        reply.send(await users.findOneBy({ id: 1 }))
})

Whereas the associated resource controller would be a little more permissive in allowing all fields to be updated by taking the user’s input directly and saving it directly to the database.

ts
app.get("/document", async (_request, reply) => {
        reply.send(await documents.findOneBy({ id: 1 }))
})

app.put("/document", async (request, reply) => {
        const result = { ...request.body as any, id: 1 }
        reply.send(await documents.save(result))
})

Here, if we take a look at our application, it is possible to retrieve a user.

GET /user

{"id":1,"name":"Dummy user","admin":false}

It is also possible to retrieve the associated document.

GET /document

{"id":1,"content":"My document content!","authorId":1}

And to update this document.

PUT /document

{"content":"Modified content"}
GET /document 

{"id":1,"content":"Modified content","authorId":1}

However, the document update feature becomes vulnerable because of the cascade property of the relation. An attacker could, when updating the document, also update the associated user by making it an administrator for example, which would not have been possible with the user’s controller.

PUT /document

{"content":"Modified content","author":{"id":1,"admin":true}}

By making this request the attacker will add the user object that corresponds to the author of the document, when saving the document, the associated user will also be saved because of the cascade property.

GET /user

{"id":1,"name":"Dummy user","admin":true}

Looking at the user, we see that his properties have been updated.

To prevent this type of risk, it is recommended to avoid cascade relations, but even more important, to check what is sent by the user. Indeed, the object for the author should never have been present when the resource was saved, it should have been filtered for authorised properties.

Conclusion

The use of ORMs can be very useful in reducing the risk of injection as well as the development and maintenance time of applications. However, this practice can also lead to security problems if not handled properly. A good practice is to use cascade properties carefully and to control user input to ensure that only authorised data is saved.