56 | {/await}
57 |
58 |
86 |
--------------------------------------------------------------------------------
/packages/lessons/build.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This module builds lessons into the app.
3 | *
4 | * @module
5 | */
6 |
7 | import type { FileSystemTree } from '@webcontainer/api';
8 | import {
9 | copyFile,
10 | mkdir,
11 | readdir,
12 | readFile,
13 | writeFile,
14 | } from 'node:fs/promises';
15 |
16 | /** Computes a path relative to this file. */
17 | const relative = (path: string) => new URL(path, import.meta.url);
18 | const dest = '../app/src/lessons/';
19 |
20 | /** Main build function, builds all directories in `lessons`. */
21 | const build = async () => {
22 | const entries = await readdir(relative('.'), { withFileTypes: true });
23 | const dirs = entries.filter((entry) => entry.isDirectory());
24 | await copyFile(relative('authors.json'), `${dest}authors.json`);
25 | return Promise.all(
26 | dirs.map((dir) => buildLesson(dir.name, relative(`${dir.name}/`))),
27 | );
28 | };
29 |
30 | /** Build a single lesson. */
31 | const buildLesson = async (name: string, dir: URL) => {
32 | console.log(`Building lesson ${name}...`);
33 | try {
34 | const files = await buildLessonFiles(dir);
35 | await mkdir(relative(`${dest}${name}`), { recursive: true });
36 | await copyFile(new URL('README.md', dir), `${dest}${name}/README.md`);
37 | await writeFile(`${dest}${name}/files.json`, JSON.stringify(files));
38 | } catch (error) {
39 | console.error(`Error building lesson ${name}: ${error}`);
40 | }
41 | };
42 |
43 | /**
44 | * Serialize all files in a format compatible with
45 | * [WebContainers](https://webcontainers.io).
46 | */
47 | const buildLessonFiles = async (dir: URL): Promise => {
48 | const entries = await readdir(dir, { withFileTypes: true });
49 |
50 | return Object.fromEntries(
51 | await Promise.all(
52 | entries.map(async (entry) => {
53 | if (entry.isDirectory()) {
54 | return [
55 | entry.name,
56 | {
57 | directory: await buildLessonFiles(new URL(`${entry.name}/`, dir)),
58 | },
59 | ];
60 | }
61 |
62 | return [
63 | entry.name,
64 | {
65 | file: {
66 | contents: await readFile(new URL(entry.name, dir), 'utf8'),
67 | },
68 | },
69 | ];
70 | }),
71 | ),
72 | );
73 | };
74 |
75 | console.time('Built lessons in');
76 | await build();
77 | console.timeEnd('Built lessons in');
78 |
--------------------------------------------------------------------------------
/packages/lessons/broken-object-property-level-authorization/README.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 'Implement Field-Level Authorization'
3 | description: 'Master the essentials of setting up field-level authorization in GraphQL resolvers for fine-grained access control.'
4 | category: 'Access Control'
5 | difficulty: 'Medium'
6 | owasp: 'API4:2023'
7 | authors: ['escape']
8 | ---
9 |
10 | This lesson is about [properly setting up](https://escape.tech/blog/authentication-authorization-access-control/#access-control-best-practices-to-secure-your-graphql-api) Object Property Level Authorization in GraphQL with Apollo. The server code is given, with authentication developed following [Apollo's recommendations](https://www.apollographql.com/docs/apollo-server/security/authentication/). Our goal is to protect sensitive data from leaking to unauthorized users.
11 |
12 | The GraphQL server of this lesson has the same structure as [Broken Object-Level Authorization](https://escape.tech/academy/broken-object-level-authorization). The data it severs is a list of users, with various details about them. Let's take a look at the data served by starting the server:
13 |
14 | - Open a new terminal.
15 | - Run `npm install` to install the dependencies.
16 | - Run `npm start` to start the server. It starts in development mode, so it will restart automatically when you make changes to the code.
17 |
18 | You should now see GraphQL IDE with the following query:
19 |
20 | ```graphql
21 | query {
22 | users {
23 | name
24 | location
25 | }
26 | }
27 | ```
28 |
29 | Running this query allows you to see the list of users and their locations. Because `location` is sensitive data we want to protect, we want to make sure that only the user themselves can see it.
30 |
31 | ```graphql
32 | # This should work
33 | query {
34 | me {
35 | name
36 | location
37 | }
38 | }
39 |
40 | # This should not
41 | query {
42 | users {
43 | name
44 | location
45 | }
46 | }
47 | ```
48 |
49 | We will improve the `location` resolver to make sure that only the user themselves can see their location:
50 |
51 | ```js
52 | import { GraphQLError } from 'graphql';
53 |
54 | export const User = {
55 | // ...
56 | location: (user, args, context) => {
57 | // Check if the user trying to access the location is the user themselves
58 | if (user.id !== context.user?.id) throw new GraphQLError('Not authorized');
59 | return user.location;
60 | },
61 | };
62 | ```
63 |
64 | And _voilà!_ The `location` field is now protected from leaking to unauthorized users. Try running the queries above to see the difference: trying to access another user's location will result in an error.
65 |
--------------------------------------------------------------------------------
/packages/app/src/lib/editor/Split.svelte:
--------------------------------------------------------------------------------
1 |
9 |
10 |
36 |
37 | {
40 | resizing = false;
41 | }}
42 | />
43 |
44 |
50 |
55 |
56 |
{
59 | resizing = true;
60 | }}
61 | />
62 |
67 |
68 |
69 |
108 |
--------------------------------------------------------------------------------
/packages/lessons/disable-debug-mode/README.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 'Disable Debug Mode for Production'
3 | description: 'Explore the importance of turning off debug mode in a production environment to safeguard against unwanted information disclosure.'
4 | category: 'Information Disclosure'
5 | difficulty: 'Easy'
6 | owasp: 'API7:2023'
7 | authors: ['escape']
8 | ---
9 |
10 | Apollo Server has its debug mode enabled by default. This is useful for development, but **it can be a security risk in production.** In this lesson, you'll learn how to disable debug mode in production.
11 |
12 | ## What is Debug Mode?
13 |
14 | This lesson contains a mini GraphQL servers that runs directly in your browser. It comes with Apollo's default settings.
15 |
16 | - Install the server by running `npm install` in a terminal.
17 | - Start the server with `npm start`.
18 |
19 | This will open a GraphQL IDE, allowing you to run queries and mutations against the server. Our schema is quite concise, and only contains a single `Lesson` type.
20 |
21 | ```graphql
22 | type Lesson {
23 | title: String!
24 | points: Int!
25 | }
26 | ```
27 |
28 | Unfortunately, our _database_ contains an error: one of the values of `points` is undefined, which is incompatible with the schema. Try querying the `lessons` field to see the error:
29 |
30 | ```graphql
31 | query {
32 | lessons {
33 | title
34 | points
35 | }
36 | }
37 | ```
38 |
39 | You should see a long error message containing the following:
40 |
41 | - The cause of the error: `Cannot return null for non-nullable field Lesson.points.`
42 | - A stack trace, which shows the location of the error in the code.
43 |
44 | The stack trace contains precious information about the **internals of our server:** details about the code architecture, the packages used, etc. **This is a security risk in production.**
45 |
46 | ## Disabling Debug Mode
47 |
48 | Fortunately for us, turning this off in production is only a matter of configuration. Most JavaScript software designed for Node.js adheres to the [`NODE_ENV`](https://nodejs.dev/en/learn/nodejs-the-difference-between-development-and-production/) environment variable, which is [usually considered as a good practice.](https://12factor.net/config) Apollo Server is no exception, and [will omit stack traces when it is set to `production`](https://www.apollographql.com/docs/apollo-server/data/errors/#omitting-or-including-stacktrace)
49 |
50 | The GraphQL server of this lesson uses a `.env` file to set environment variables at runtime. If your server runs in a container, you should use [environment variables](https://docs.docker.com/compose/environment-variables/) instead.
51 |
52 | Set `NODE_ENV=production` in the `.env` file, and restart the server. Rerun the previous query:
53 |
54 | ```graphql
55 | query {
56 | lessons {
57 | title
58 | points
59 | }
60 | }
61 | ```
62 |
63 | The error message should now be much shorter, and the stack trace should be gone. **No more stack traces in production!**
64 |
--------------------------------------------------------------------------------
/packages/app/src/routes/Header.svelte:
--------------------------------------------------------------------------------
1 |
26 |
27 |
81 | {/if}
82 |
83 |
110 |
--------------------------------------------------------------------------------
/packages/lessons/cors/README.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 'Configure HTTP Headers for User Protection'
3 | description: 'Understand the role of the Access-Control-Allow-Origin header in safeguarding users from cross-site request forgery (CSRF) attacks.'
4 | category: 'HTTP'
5 | difficulty: 'Medium'
6 | owasp: 'API7:2023'
7 | authors: ['escape']
8 | ---
9 |
10 | Modern browsers all feature new security mechanisms offering developers ways to protect their users from online threats. A common threat is [cross-site request forgery (CSRF)](https://escape.tech/blog/understanding-and-dealing-with-cross-site-request-forgery-attacks/), which is an attack that tricks a user into performing an action they didn't intend to do. In this lesson, you'll learn how to setup the `Access-Control-Allow-Origin` header to protect your users from CSRF attacks.
11 |
12 | ## Cross-site request forgery
13 |
14 | This lesson contains two websites: a vulnerable GraphQL server (in `src/`) and an evil website (in `evil/`). The vulnerable server is a simple GraphQL server that allows users to register to a forum. The evil website's goal is to trick users into registering to the forum without their consent.
15 |
16 | To start the servers, run:
17 |
18 | - `npm install` and `npm start` in one terminal.
19 | - `npm run evil` in another terminal.
20 |
21 | The two websites should open. Try creating an account on the forum, then copy the URL of the forum into the evil website. After refreshing the list of users, you should now see an _Evil >:}_ account at the bottom of the list.
22 |
23 | The evil website performed a CSRF attack. It used your browser to send a request to the vulnerable server, which created an account for you without your consent. This is possible because the vulnerable server doesn't protect itself against CSRF attacks.
24 |
25 | ## Access-Control-Allow-Origin
26 |
27 | Fortunately, modern browsers offer a way to prevent these malicious requests: the `Access-Control-Allow-Origin` header. This header allows a server to specify which websites are allowed to send requests to it. If a website tries to send a request to a server that doesn't allow it, the browser will block the request.
28 |
29 | The [cors](https://www.npmjs.com/package/cors) package takes care of sending the header for us, but we need to configure it first. Open `src/index.js` and update the `app.post('/graphql', ...)` line:
30 |
31 | ```js
32 | app.post(
33 | '/graphql',
34 | // Copy the value after `Server origin:` on the vulnerable server
35 | cors({ origin: '' }),
36 | bodyParser.json(),
37 | expressMiddleware(server),
38 | );
39 | ```
40 |
41 | This header tells all browsers to block requests coming from any website other than the right one. The evil website has a different origin (which is the combination of HTTP scheme, hostname and port) than the vulnerable server, so the browser will block the request.
42 |
43 | The vulnerable server should restart automatically. Try to perform the attack again from the evil website: the request fails with a network error. **Our server is now safe from CSRF attacks!**
44 |
--------------------------------------------------------------------------------
/packages/app/src/lib/editor/icons/categories/Injection.svelte:
--------------------------------------------------------------------------------
1 |
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # API Security Academy by Escape.tech
2 |
3 | ## What is it?
4 |
5 | API Security Academy provides hands-on, interactive lessons that teach various vulnerabilities and best practices in GraphQL security. Discover its full learning potential directly in your browser. Each lesson features a WebContainer with a live GraphQL application, demonstrating not just the risks but also how to **exploit** and **fix** them.
6 |
7 | > 💡 The [API Security Academy](https://www.escape.tech/academy?utm_source=github&utm_medium=social&utm_campaign=description-call-out) is accessible for free. We initially chose to prioritize GraphQL, as it’s at the core of our expertise, but anticipate introducing additional API types in the future!
8 |
9 | ## Why learn with API Security Academy?
10 |
11 | - 📚 Comprehensive lessons: Covering everything from basic to advanced GraphQL security topics.
12 | - 💻 Interactive: Each lesson includes a WebContainer for a real-world experience.
13 | - 🧑💻 For Developers and Security Engineers: Whether you're building or securing GraphQL apps, there's always more to explore and learn.
14 | - 🆓 Free and Open Source: Learn at your own pace, and even contribute to make it better!
15 |
16 | ## Features
17 |
18 | - 🌐 Browser-Based learning: no downloads, installs, or account creation. Start learning immediately right within your browser.
19 | - 🛠 Hands-on experience: apply your knowledge in real-world GraphQL app scenarios.
20 |
21 | ## How to contribute
22 |
23 | We're thrilled that you're interested in contributing to the API Security Academy! Contributions are essential for keeping this project informative, up-to-date, and, most importantly, beneficial for everyone interested in GraphQL and Application security.
24 |
25 | This project consists of two main components:
26 |
27 | - [`app`](./packages/app/): A Svelte-based IDE that operates directly in your web browser.
28 | - [`lessons`](./packages/lessons/): This directory houses all the tutorial content.
29 |
30 | ### What is a "Lesson"?
31 |
32 | A lesson in API Security Academy is structured as a regular `npm` package, containing at least a `package.json` file and a `README.md` file. The README is Svelte-enhanced markdown that drives the lesson content.
33 |
34 | ### Quick Start Guide
35 |
36 | If you're eager to contribute, here's how you can get started:
37 |
38 | ```bash
39 | # Clone the GitHub repository
40 | git clone https://github.com/Escape-Technologies/graphql-security-academy.git
41 | cd academy
42 | # Use yarn to install all necessary dependencies
43 | yarn install
44 | # Launch the development environment
45 | yarn dev
46 | ```
47 |
48 | Now, you should have a local instance of API Security Academy running. Feel free to make any changes and test them out.
49 |
50 | ### Contribution ideas
51 |
52 | - Writing new lessons or updating existing ones.
53 | - Enhancing the UI/UX of the `app` component.
54 | - Reporting bugs and suggesting new features.
55 |
56 | Feel free to submit a pull request or create an issue to discuss any changes you have in mind.
57 |
58 | Thank you for contributing to making GraphQL more secure!
59 |
60 | > And hurry up to start your first lesson [here](https://escape.tech/academy/broken-authentication?utm_source=github&utm_medium=social)!
61 |
--------------------------------------------------------------------------------
/packages/app/src/app.scss:
--------------------------------------------------------------------------------
1 | @use '@fontsource-variable/inter';
2 | @import 'modern-normalize/modern-normalize.css';
3 |
4 | :root {
5 | // Theme
6 | --dark: #eee;
7 | --main: #fff;
8 | --inactive: #35314c10;
9 | --focused: #fff;
10 | --hovered: #6c678f40;
11 | --text: #333;
12 | --accent: #05e2b7;
13 | --secondary-accent: #6762d5;
14 | --weak-color: #babbbb;
15 | --bg-secondary: #f2f2f2;
16 | --bg-hover: #e7e7e7;
17 |
18 | // Main colors
19 | --bg: var(--main);
20 | --color: var(--text);
21 | --link: var(--accent);
22 |
23 | // Markdown colors
24 | --prism-color: #1c1b1d;
25 | --prism-bg: #fafaff;
26 | --prism-highlight: #e9e9ff;
27 | --prism-function: #0077bf;
28 | --prism-keyword: #5c33bd;
29 | --prism-class: #ab6000;
30 | --prism-scalar: #bd2a33;
31 | --prism-string: #4e7e06;
32 | --prism-comment: #355454;
33 |
34 | line-height: 1.5;
35 | cursor: default;
36 | scrollbar-width: thin;
37 | }
38 |
39 | ::selection {
40 | background-color: #05e2b780;
41 | }
42 |
43 | ::-webkit-scrollbar {
44 | width: 4px;
45 | height: 4px;
46 | }
47 |
48 | ::-webkit-scrollbar-track {
49 | background: transparent;
50 | }
51 |
52 | ::-webkit-scrollbar-thumb {
53 | background-color: rgb(155 155 155 / 35%);
54 | border: transparent;
55 | border-radius: 20px;
56 | }
57 |
58 | * {
59 | outline: 0 solid transparent;
60 | transition: outline 0.1s ease-out;
61 |
62 | &:focus-visible {
63 | outline: 0.125em solid #07c9ac80;
64 | }
65 | }
66 |
67 | body {
68 | font-family: Inter, sans-serif;
69 | color: var(--color);
70 | background-color: var(--bg);
71 | }
72 |
73 | h1,
74 | h2,
75 | h3,
76 | h4,
77 | h5,
78 | h6 {
79 | font-family: Inter, sans-serif;
80 | }
81 |
82 | h1 {
83 | font-size: 1.8em;
84 | line-height: 1.25;
85 | }
86 |
87 | h2 {
88 | font-size: 1.5em;
89 | line-height: 1.33;
90 | }
91 |
92 | h3 {
93 | font-size: 1.25em;
94 | }
95 |
96 | a {
97 | color: inherit;
98 |
99 | &:hover,
100 | &:focus {
101 | color: var(--link);
102 | }
103 | }
104 |
105 | .icon {
106 | flex-shrink: 0;
107 | vertical-align: bottom;
108 |
109 | h1 &,
110 | h2 &,
111 | h3 & {
112 | width: 1.25em;
113 | height: 1.25em;
114 | }
115 | }
116 |
117 | :where(button, input) {
118 | display: inline-flex;
119 | gap: 0.25em;
120 | align-items: center;
121 | padding: 0.375em;
122 | color: #838383;
123 | background: #f2f2f2;
124 | border: 0.125em solid #f2f2f2;
125 | border-radius: 0.25em;
126 | outline: 0 solid transparent;
127 | transition: all 0.1s ease-in-out;
128 | }
129 |
130 | input {
131 | border-color: #e2e2e2;
132 |
133 | &:enabled {
134 | color: #000;
135 | background: #fff;
136 |
137 | &:hover,
138 | &:focus {
139 | border-color: #acacac;
140 | }
141 |
142 | &:focus-visible {
143 | outline: 0.25em solid #e2e2e280;
144 | }
145 | }
146 | }
147 |
148 | :where(button:enabled) {
149 | color: var(--text);
150 | background: #05e2b7;
151 | border-color: #05e2b7;
152 |
153 | &:where(:hover) {
154 | background: #04c19c;
155 | border-color: #04c19c;
156 | }
157 |
158 | &:where(:active) {
159 | background: #06ab8a;
160 | border-color: #06ab8a;
161 | }
162 |
163 | &:where(:focus-visible) {
164 | outline: 0.25em solid #05e2b780;
165 | }
166 | }
167 |
--------------------------------------------------------------------------------
/packages/lessons/broken-authentication/README.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 'Prevent Mutation Brute-Force Attacks'
3 | description: 'Understand how to mitigate brute-force attacks targeting GraphQL authentication mutations for enhanced security.'
4 | category: 'Access Control'
5 | difficulty: 'Easy'
6 | owasp: 'API2:2023'
7 | authors: ['escape']
8 | ---
9 |
10 | Because the authentication mechanisms are exposed and complex, they are a target of choice for attackers. This lesson will show you how lesser-known GraphQL features can be used against your application.
11 |
12 | Start the GraphQL server with `npm install` and `npm start` to get started.
13 |
14 | ## GraphQL Aliasing
15 |
16 | Aliasing is a GraphQL feature allowing a developer to rename a field in the resulting object. For example, the following query:
17 |
18 | ```graphql
19 | query {
20 | myAlias: users {
21 | name
22 | }
23 | }
24 | ```
25 |
26 | Will return the following object:
27 |
28 | ```json
29 | {
30 | "data": {
31 | "myAlias": [
32 | {
33 | "name": "alice"
34 | }
35 | ]
36 | }
37 | }
38 | ```
39 |
40 | In the results, the field is now named `myAlias` instead of its original name `users`. This also allows a developer to query the same field several times, for different purposes:
41 |
42 | ```graphql
43 | query {
44 | users {
45 | name
46 | myAlias: name
47 | }
48 | }
49 | ```
50 |
51 | This query will show `"alice"` twice in the results.
52 |
53 | An attacker can exploit aliasing to run the same mutation several times:
54 |
55 | ```graphql
56 | mutation {
57 | attempt1: login(name: "alice", password: "password1")
58 | attempt2: login(name: "alice", password: "password2")
59 | attempt3: login(name: "alice", password: "password3")
60 | attempt4: login(name: "alice", password: "password4")
61 | attempt5: login(name: "alice", password: "password5")
62 | # ...
63 | }
64 | ```
65 |
66 | An attacker is able to send several login attempts in a single request, effectively brute-forcing Alice's password.
67 |
68 | ## Installing GraphQL Armor
69 |
70 | [GraphQL Armor](https://github.com/Escape-Technologies/graphql-armor) is a library that helps you protect your GraphQL API from malicious queries and mutations. It runs on all major GraphQL engines without configuration, and adds various security features to your GraphQL API.
71 |
72 | This attack is one of the many that GraphQL Armor can protect you from.
73 |
74 | Your goal is to install GraphQL Armor and protect your server from the attack: install GraphQL Armor with `npm install @escape.tech/graphql-armor`.
75 |
76 | Having GraphQL Armor to work is as simple as two lines of code:
77 |
78 | ```js
79 | import { ApolloArmor } from '@escape.tech/graphql-armor';
80 |
81 | // Instantiate GraphQL Armor
82 | const armor = new ApolloArmor({
83 | // Completely disable aliases for this lesson
84 | maxAliases: { n: 0 },
85 | });
86 |
87 | const server = new ApolloServer({
88 | typeDefs,
89 | resolvers,
90 | // Add the protections to the server
91 | ...armor.protect(),
92 | });
93 | ```
94 |
95 | You can try to re-run the attack, it should now fail, with an error stating that your query contains too many aliases. **Our server is now protected from aliasing brute-force!** You can read more about GraphQL Armor and all its protections [on GitHub](https://github.com/Escape-Technologies/graphql-armor).
96 |
97 | > Need extra help to get started with GraphQL Armor? Check out [GraphQL Armor docs](https://escape.tech/graphql-armor/docs/getting-started).
98 |
--------------------------------------------------------------------------------
/packages/lessons/unrestricted-resource-consumption/README.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 'Limit Query Complexity'
3 | description: 'Discover techniques to restrict expensive queries using GraphQL Armor, enhancing performance and security.'
4 | category: 'Complexity'
5 | difficulty: 'Easy'
6 | owasp: 'API4:2023'
7 | authors: ['escape']
8 | ---
9 |
10 | GraphQL engines ship without complexity limits by default, allowing you to ship complex applications rapidly, but also enabling attackers to perform expensive queries. Cycles in the graph [can lead to arbitrarily deep queries](https://escape.tech/blog/cyclic-queries-and-depth-limit/), which cause degraded performance or even denial of service.
11 |
12 | This lesson contains a _very_ simple social network with users and friends. Because users have friends and friends have friends, it is possible to construct a query that will cause the server to run out of memory:
13 |
14 | ```graphql
15 | query {
16 | user(id: 1) {
17 | friends {
18 | friends {
19 | friends {
20 | friends {
21 | friends {
22 | # ... as deep as the attacker wants
23 | }
24 | }
25 | }
26 | }
27 | }
28 | }
29 | }
30 | ```
31 |
32 | Let's start this lesson by performing this attack:
33 |
34 | - Install the server and its dependencies by running `npm install` in a terminal.
35 | - Start the server by running `npm start` in a terminal. It starts in development mode, so it will restart automatically when you make changes to the code.
36 |
37 | A GraphQL ID should open, use it to send a deep query to the server.
38 |
39 | Note: the server is running directly inside your browser, don't make it crash!
40 |
41 | ## Installing GraphQL Armor
42 |
43 | [GraphQL Armor](https://github.com/Escape-Technologies/graphql-armor) is a library that helps you protect your GraphQL API from malicious queries and mutations. It runs on all major GraphQL engines without configuration, and adds various security features to your GraphQL API.
44 |
45 | This attack is one of the many that GraphQL Armor can protect you from.
46 |
47 | Your goal is to install GraphQL Armor and protect your server from the attack: install GraphQL Armor with `npm install @escape.tech/graphql-armor`.
48 |
49 | Having GraphQL Armor work is as simple as two lines of code:
50 |
51 | ```js
52 | import { ApolloArmor } from '@escape.tech/graphql-armor';
53 |
54 | // Instantiate GraphQL Armor
55 | const armor = new ApolloArmor();
56 |
57 | const server = new ApolloServer({
58 | typeDefs,
59 | resolvers,
60 | // Add the protections to the server
61 | ...armor.protect(),
62 | });
63 | ```
64 |
65 | Try running the following query now that you have Armor installed:
66 |
67 | ```graphql
68 | query {
69 | user(id: 1) {
70 | friends {
71 | friends {
72 | friends {
73 | friends {
74 | friends {
75 | name
76 | }
77 | }
78 | }
79 | }
80 | }
81 | }
82 | }
83 | ```
84 |
85 | You should now see an error stating that your query is too deep.
86 |
87 | The default limit is 6, which is enough for most applications. You can change it by passing a configuration object to `ApolloArmor`:
88 |
89 | ```js
90 | // Set maximum depth to 10
91 | const armor = new ApolloArmor({ maxDepth: { n: 10 } });
92 | ```
93 |
94 | **Our server is now safe from arbitrary deep queries!** You can read more about GraphQL Armor and all its protections [on GitHub](https://github.com/Escape-Technologies/graphql-armor).
95 |
--------------------------------------------------------------------------------
/packages/app/src/routes/[lesson]/Readme.svelte:
--------------------------------------------------------------------------------
1 |
26 |
27 |
28 | {#if showConfetti}
29 |
33 |
34 |
35 | {/if}
36 |
37 |
{readme.metadata.title}
38 |
39 |
63 |
64 |
65 |
66 |
93 |
94 |
95 |
133 |
--------------------------------------------------------------------------------
/packages/lessons/broken-function-level-authorization/README.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 'Implement Resolver-Level Authorization'
3 | description: 'Master techniques for setting up authorization specifically tailored for GraphQL mutations, enhancing both data integrity and security.'
4 | category: 'Access Control'
5 | difficulty: 'Hard'
6 | owasp: 'API5:2023'
7 | authors: ['escape']
8 | ---
9 |
10 | This lesson is about [properly setting up](https://escape.tech/blog/authentication-authorization-access-control/#access-control-best-practices-to-secure-your-graphql-api) function-level authorization in GraphQL with Apollo. The server code is given, with authentication developed following [Apollo's recommendations](https://www.apollographql.com/docs/apollo-server/security/authentication/). Small oversights have made **the authorization mechanism vulnerable**. Our goal is to exploit it and then fix it.
11 |
12 | ## The vulnerable server
13 |
14 | The GraphQL server of this lesson has the same structure as _Broken Object-Level Authorization_. The data it severs is a list of users, each user authoring posts. Let's take a look at the data served by starting the server:
15 |
16 | - Open a new terminal.
17 | - Run `npm install` to install the dependencies.
18 | - Run `npm start` to start the server. It starts in development mode, so it will restart automatically when you make changes to the code.
19 |
20 | You should now see GraphQL IDE with the following query:
21 |
22 | ```graphql
23 | query {
24 | users {
25 | name
26 | posts {
27 | title
28 | }
29 | }
30 | }
31 | ```
32 |
33 | Running this query allows you to see the list of users and their posts.
34 |
35 | You can also, when logged in (you should be logged in as Eve with `Authorization: Bearer 3`), create and delete posts:
36 |
37 | ```graphql
38 | # Create a post
39 | mutation {
40 | createPost(title: "New post from Eve") {
41 | id # This should return 3
42 | authorId
43 | title
44 | }
45 | }
46 |
47 | # And delete it
48 | mutation {
49 | deletePost(id: "3")
50 | }
51 | ```
52 |
53 | ## Missing authorization
54 |
55 | While developing the `deletePost` mutation, the developer forgot to add authorization to the resolver. This means that anyone can delete any post, even if they are not the author. This is a **broken function-level authorization**.
56 |
57 | You can try deleting Alice's and Bob's posts while logged in as Eve:
58 |
59 | ```graphql
60 | mutation {
61 | alice: deletePost(id: "1")
62 | bob: deletePost(id: "2")
63 | }
64 | ```
65 |
66 | To prevent this, we need to add authorization to the resolver.
67 |
68 | Our server already features a simple authentication mechanism. For instance, it is used when creating posts:
69 |
70 | ```js
71 | export const Mutation = {
72 | // `context` contains a `user` key if the user is logged in
73 | createPost: (_, args, context) => {
74 | // If the user is not logged in, throw an error
75 | if (!context.user) throw new GraphQLError('Not authorized');
76 | // Otherwise, create the post with the current user as the author
77 | return createPost(args.title, context.user.id);
78 | },
79 | // ...
80 | };
81 | ```
82 |
83 | We can leverage `context.user` to ensure that the user that tries to delete the post is the author:
84 |
85 | ```js
86 | import { getPost } from './database.js';
87 |
88 | export const Mutation = {
89 | // ...
90 | deletePost: (_, args, context) => {
91 | const post = getPost(args.id);
92 |
93 | // If the user is not the author, throw an error
94 | if (post && post.authorId !== context.user?.id)
95 | throw new GraphQLError('Not authorized');
96 |
97 | return deletePost(args.id);
98 | },
99 | };
100 | ```
101 |
102 | You can try deleting Alice's post again, it won't work anymore. You can only delete your own posts, **our broken function-level authorization is now fixed!**
103 |
--------------------------------------------------------------------------------
/packages/app/src/assets/cup.svg:
--------------------------------------------------------------------------------
1 |
67 |
--------------------------------------------------------------------------------
/packages/lessons/json-injection/README.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 'Validate JSON Inputs'
3 | description: 'Explore techniques for validating JSON input objects to mitigate the risk of injection vulnerabilities.'
4 | category: 'Injection'
5 | difficulty: 'Medium'
6 | owasp: 'API8:2019'
7 | authors: ['escape']
8 | ---
9 |
10 | Using `JSON` as a GraphQL input object can lead to vulnerabilities. This tutorial will show how one can exploit a `JSON` input object to **perform arbitrary SQL queries** with [Prisma](https://www.prisma.io) ORM, and **how to prevent it.**
11 |
12 | This lesson does not currently work in the browser for security reasons. You can run it locally with the following steps:
13 |
14 | - Clone the repository: `git clone https://github.com/Escape-Technologies/graphql-security-academy.git`
15 | - Run `cd learn/packages/lessons/json-injection`
16 | - Install dependencies with `yarn install`
17 | - Continue from here with `yarn start`, `yarn exploit`, etc.
18 |
19 | _More details on [prisma#17710](https://github.com/prisma/prisma/issues/17710)_
20 |
21 | ## The vulnerability
22 |
23 | This tutorial contains a GraphQL API that uses [Prisma](https://www.prisma.io) ORM to perform search queries in a SQLite database. It has one single resolver, `findUsers`, that takes a `JSON` input object as argument. The `JSON` input object is used to filter the users in the database.
24 |
25 | ```graphql
26 | # GraphQL schema
27 | type Query {
28 | findUsers(where: JSON): [User]
29 | # ^^^^ arbitrary JSON input object
30 | }
31 | ```
32 |
33 | ```js
34 | // GraphQL resolver
35 | Query: {
36 | findUsers: (_, { where }) => prisma.user.findMany({ where }),
37 | // ^^^^^ arbitrary object...
38 | // ...leading to arbitrary queries ^^^^^
39 | }
40 | ```
41 |
42 | Our database table contains more columns than what the API exposes. Indeed, there is an `apiKey` used for authentication purposes, and while an attack cannot read it directly, it can leverage Prisma's API to get it.
43 |
44 | ```graphql
45 | # Legitimate query
46 | query {
47 | findUsers(where: { email: { endsWith: "@example.com" } }) {
48 | email
49 | }
50 | }
51 |
52 | # Malicious query
53 | query {
54 | findUsers(where: { apiKey: { startsWith: "0" } }) {
55 | email
56 | }
57 | }
58 | ```
59 |
60 | - Start the API with `npm install` and `npm start`.
61 | - Run the exploit in `exploit/index.ts` with `npm run exploit`.
62 |
63 | ## How to prevent it
64 |
65 | We will use [zod](https://zod.dev/) to validate the `JSON` input object. Zod is a TypeScript library that allows to define a schema for a JavaScript object, and to validate it.
66 |
67 | ```ts
68 | Query: {
69 | findUsers: (_, { where }) => {
70 | // Allows equality, contains, startsWith and endsWith filters on columns
71 | const filterSchema = z.union([
72 | z.string(),
73 | z
74 | .object({
75 | contains: z.string(),
76 | startsWith: z.string(),
77 | endsWith: z.string(),
78 | })
79 | .partial(),
80 | ]);
81 | // Restrict to firstName, lastName and email
82 | const whereSchema = z
83 | .object({
84 | firstName: filterSchema,
85 | lastName: filterSchema,
86 | email: filterSchema,
87 | })
88 | .partial();
89 |
90 | // Validate the input object
91 | const results = whereSchema.safeParse(where);
92 |
93 | if (results.success) {
94 | // Use the safe version of the input objects
95 | return prisma.user.findMany({ where: results.data });
96 | } else {
97 | // Return an error in case the input object is invalid
98 | return new GraphQLError('Validation error');
99 | }
100 | },
101 | }
102 | ```
103 |
104 | - Install zod with `npm install zod`.
105 | - Use the zod schema that allows searching with `email`, `firstName` and `lastName` fields, but not others.
106 |
107 | You can now try to run the exploit again. It should not work anymore since Zod strips all unknown fields like `apiKey`.
108 |
--------------------------------------------------------------------------------
/packages/app/src/lib/editor/icons/categories/Dos.svelte:
--------------------------------------------------------------------------------
1 |
38 |
--------------------------------------------------------------------------------
/packages/app/src/routes/Progress.svelte:
--------------------------------------------------------------------------------
1 |
18 |
19 |
20 | {Math.ceil($progress)}/{total}
21 |
22 |
23 |
24 |
25 |
= total}>
26 |
27 |
28 |
29 |
30 |
31 | {#if done >= total}
32 |
33 |
34 |
= total}>
35 |
39 |
40 |
41 |
Congratulations! You have done all lessons
42 |
43 | You can post your certification of completion on your LinkedIn
44 | profile.
45 |
49 | API Security Academy provides hands-on, interactive lessons that teach
50 | various vulnerabilities and best practices in GraphQL security. You can
51 | access its full learning potential directly in your browser.
52 |
53 |
54 | Each lesson is built around a WebContainer containing a live GraphQL
55 | application, so you'll not only understand why a vulnerability is
56 | risky, but also
57 | how to exploit it and, most importantly, how to fix it.
58 |
69 | You can support the project by leaving a star, reporting bugs or even
70 | creating new lessons. We welcome all contributions!
71 |
72 |
73 |
74 |
75 |
76 | Feel free to jump in and start your first lesson. Happy learning! 🔒
77 |
78 |
79 |
80 |
81 |
Lessons
82 |
83 |
87 |
88 |
89 |
106 |
107 |
143 |
--------------------------------------------------------------------------------
/packages/app/src/lib/editor/icons/categories/AccessControl.svelte:
--------------------------------------------------------------------------------
1 |
40 |
--------------------------------------------------------------------------------
/packages/app/src/lib/editor/icons/categories/InformationDiclosure.svelte:
--------------------------------------------------------------------------------
1 |
122 |
--------------------------------------------------------------------------------
/packages/lessons/broken-object-level-authorization/README.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 'Implement Object-Level Authorization'
3 | description: 'Master the essentials of object-level authorization for secure data access and improved application security.'
4 | category: 'Access Control'
5 | difficulty: 'Easy'
6 | owasp: 'API1:2023'
7 | authors: ['escape']
8 | ---
9 |
10 | If this is the first lesson you are doing, welcome! This learning platform is developed conjointly by [Escape](https://escape.tech/) and [the open-source community](https://github.com/Escape-Technologies/graphql-security-academy). All the content of this site is open-source, and contributions are welcome.
11 |
12 | This lesson is about properly setting up object-level authorization in GraphQL with Apollo. The server code is given, with authentication developed following [Apollo's recommendations](https://www.apollographql.com/docs/apollo-server/security/authentication/). Small oversights have made **the authorization mechanism vulnerable**. Our goal is to exploit it and then fix it.
13 |
14 | ## The vulnerable server
15 |
16 | The GraphQL server of this lesson is made of 4 files:
17 |
18 | - `index.js` is the entry point of the server. It creates the Apollo server with a schema and starts it.
19 | - `resolvers.js` contains the resolvers of the GraphQL schema.
20 | - `context.js` contains the function that creates the context of each request. It allows authentication of the current user.
21 | - `database.js` contains a mock database of users and posts.
22 |
23 | The data served by the GraphQL server is a list of posts, each post having an author. Let's take a look at the data served by starting the server:
24 |
25 | - Open a new terminal.
26 | - Run `npm install` to install the dependencies.
27 | - Run `npm start` to start the server. It starts in development mode, so it will restart automatically when you make changes to the code.
28 |
29 | You should now see GraphQL IDE with the following query:
30 |
31 | ```graphql
32 | query {
33 | users {
34 | name
35 | posts {
36 | title
37 | }
38 | }
39 | }
40 | ```
41 |
42 | Running this query allows you to see the list of users and their **published** posts. Is there a way to access the **unpublished** posts of a user?
43 |
44 | ## Missing authorization
45 |
46 | As an attacker, the first step is usually to collect information about the target. Let's try to add all the fields possible to the query above:
47 |
48 | ```graphql
49 | query {
50 | users {
51 | id # One more field here
52 | name
53 | posts {
54 | id # Two more there
55 | published
56 | title
57 | }
58 | }
59 | }
60 | ```
61 |
62 | We now notice something very interesting: ids are sequential. This means that we can easily guess the id of a post: here `2` is missing, let's try to access it:
63 |
64 | ```graphql
65 | query {
66 | post(id: "2") {
67 | id
68 | authorId
69 | title
70 | published
71 | }
72 | }
73 | ```
74 |
75 | **It worked!** In all its glory, here is the unpublished post:
76 |
77 | ```json
78 | {
79 | "data": {
80 | "post": {
81 | "id": "2",
82 | "authorId": "1",
83 | "title": "",
84 | "published": false
85 | }
86 | }
87 | }
88 | ```
89 |
90 | Our server lacks object-level authorization, and this allows any attacker to access unpublished data. Let's fix it!
91 |
92 | ## Limiting access to published posts
93 |
94 | Since the beginning of this lesson, we issue requests as Eve, whose id is 3. The server identify us thanks to the `Authorization: Bearer 3` header, defined in the bottom left corner of the IDE. Eve should not be able to access Alice's unpublished posts.
95 |
96 | Our authentication mechanism is rather simple, but enough for this lesson. It is implemented in `context.js`. The `getContext` functions returns either an object containing the current user, or an empty if the user is not authenticated. This object is then passed to the resolvers as the `context` argument, in third position.
97 |
98 | We already have a resolver that relies on authentication: `me`. You can see that you are properly identified as Eve by running the following query:
99 |
100 | ```graphql
101 | query {
102 | me {
103 | id
104 | name
105 | }
106 | }
107 | ```
108 |
109 | We can use the context argument to check if the user accessing an unpublished post is its author. If not, we can return an error. Let's do that in `resolvers.js`:
110 |
111 | ```js
112 | export const Query = {
113 | // ...
114 |
115 | // Get the query context as the third argument
116 | post: (_, args, context) => {
117 | const post = getPost(args.id);
118 | if (!post) throw new GraphQLError('Post not found');
119 |
120 | // Refuse to return the post if it is unpublished and the user is not its author
121 | if (!post.published && post.authorId !== context.user?.id)
122 | throw new GraphQLError('Unauthorized');
123 |
124 | return post;
125 | },
126 | };
127 | ```
128 |
129 | Running this query as Eve (`Authorization: Bearer 3`) will now throw an error, whereas running it as Alice (`Authorization: Bearer 1`) will properly return the post. **No more unauthorized access!**
130 |
131 | Want to learn further about Access Control vulnerability? Check out [this article](https://escape.tech/blog/authentication-authorization-access-control/).
132 |
--------------------------------------------------------------------------------
/packages/lessons/sql-injection/README.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 'Combat SQL Injections'
3 | description: 'Delve into the mechanisms of SQL injections—how to exploit them and the strategies for preventing this enduring vulnerability.'
4 | category: 'Injection'
5 | difficulty: 'Easy'
6 | owasp: 'API8:2019'
7 | authors: ['escape']
8 | ---
9 |
10 | A SQL injection is a common attack vector that allows an attacker to execute arbitrary SQL queries on a database. This can be used to steal data, modify data, or even execute arbitrary code on the database server. This lesson focuses on a privilege escalation attack [leveraging a SQL injection](https://escape.tech/blog/sql-injection-in-graphql/).
11 |
12 | This lesson comes with a GraphQL server offering a login and registration service. To start the server, run the following command:
13 |
14 | - Install the dependencies with `npm install`
15 | - Start the server with `npm start`. The server will start in development mode: it will restart automatically when you make changes to the code and the database will be reset at each restart.
16 |
17 | A GraphQL IDE should open, allowing you to query the server.
18 |
19 | ## The vulnerability
20 |
21 | The server offers a query, `users`, allowing to retrieve the list of users, and two mutations, `login` and `register`, allowing to authenticate and register new users.
22 |
23 | Try creating an account with the following credentials:
24 |
25 | ```graphql
26 | mutation {
27 | register(email: "user@example.com", password: "password1")
28 | }
29 | ```
30 |
31 | You can see that the user has been created by querying the list of users:
32 |
33 | ```graphql
34 | query {
35 | users {
36 | email
37 | admin
38 | }
39 | }
40 | ```
41 |
42 | And you can authenticate with the following mutation:
43 |
44 | ```graphql
45 | mutation {
46 | login(email: "user@example.com", password: "password1") {
47 | id
48 | email
49 | admin
50 | }
51 | }
52 | ```
53 |
54 | Our goal is to exploit the `register` mutation to create an administrator account. The code behind the `register` mutation is the following:
55 |
56 | ```js
57 | db.run(
58 | `INSERT INTO users (email, password, admin) VALUES ('${email}', '${password}', 0)`,
59 | (err) => {
60 | // ...
61 | },
62 | );
63 | ```
64 |
65 | If you look closely, you can see that the email and password are directly injected into the SQL query. **This is a SQL injection vulnerability.** We can exploit it by including a string terminator `'` in the email, and then arbitrary SQL. For instance, registering with the following credentials:
66 |
67 | ```graphql
68 | mutation {
69 | register(
70 | # v We include a string terminator in the email
71 | email: "eve@example.com', '$argon2id$v=19$m=1024,t=1,p=1$Q3FMVVlaNlNjc1dIZWlQcg$p8om9rk2ef3+uzO8Hg0Gwhnx1+1vIll2qDiiWBACrUw', 1); --"
72 | # ^ We provide a password hash compatible with the server ^
73 | # We set the admin flag to 1 |
74 | password: "password1"
75 | )
76 | }
77 | ```
78 |
79 | The resulting query will be: (with added newlines for readability)
80 |
81 | ```sql
82 | INSERT INTO users (email, password, admin)
83 | VALUES (
84 | 'eve@example.com',
85 | '$argon2id$v=19$m=1024,t=1,p=1$Q3FMVVlaNlNjc1dIZWlQcg$p8om9rk2ef3+uzO8Hg0Gwhnx1+1vIll2qDiiWBACrUw',
86 | 1
87 | ); --', '$argon2id$v=19$m=1024,t=1,p=1$T1p2cTlBa0lmVg$kaoMvhUXvLxKXisdaky0kGZU2hoKD5tncvkKQkANagU', 0)
88 | ```
89 |
90 | You can see our malicious payload completely overriding the initial query. The `--` at the end of the query is a comment, allowing the resulting SQL to be valid. The comment contains the rest of the query, `', '${password}', 0)` in the JavaScript code above.
91 |
92 | We can query the list of users again to see if our malicious account has been created:
93 |
94 | ```graphql
95 | query {
96 | users {
97 | email
98 | admin
99 | }
100 | }
101 | ```
102 |
103 | It worked! We can now authenticate with our new account:
104 |
105 | ```graphql
106 | mutation {
107 | login(email: "eve@example.com", password: "password1") {
108 | id
109 | email
110 | admin
111 | }
112 | }
113 | ```
114 |
115 | ## Preventing SQL injections
116 |
117 | There are many ways to [prevent SQL injections](https://escape.tech/blog/sql-injection-in-graphql/#how-to-remediate). Most modern ORMs and database libraries will automatically escape the values you provide to prevent SQL injections. We use [sqlite3](https://www.npmjs.com/package/sqlite3) in this lesson, and it does feature an escaping mechanism: we use the `?` placeholder to escape the values we provide to the query, and provide the values separately as an array.
118 |
119 | Replace the database functions in `src/database.js` with the following:
120 |
121 | ```js
122 | export const createUser = async (email, password) =>
123 | new Promise((resolve, reject) => {
124 | db.run(
125 | // Use the ? placeholder to escape the values
126 | 'INSERT INTO users (email, password, admin) VALUES (?, ?, 0)',
127 | // Provide the values as an array
128 | [email, password],
129 | (err) => {
130 | if (err) reject(err);
131 | else resolve(undefined);
132 | },
133 | );
134 | });
135 |
136 | export const getUserByEmail = async (email) =>
137 | new Promise((resolve, reject) => {
138 | // Same here
139 | db.all('SELECT * FROM users WHERE email = ?', [email], (err, rows) => {
140 | if (err) reject(err);
141 | else resolve(rows[0]);
142 | });
143 | });
144 | ```
145 |
146 | You can retry the attack, but this time it won't work: **our server is now safe from SQL injections!** It, however still lacks proper validation mechanisms for the email, allowing us to register with an invalid email address, but this is the subject of another lesson.
147 |
--------------------------------------------------------------------------------
/packages/app/src/lib/editor/Editor.svelte:
--------------------------------------------------------------------------------
1 |
125 |
126 | {
128 | if (event.key === 's' && (event.metaKey || event.ctrlKey)) {
129 | event.preventDefault();
130 | await save();
131 | }
132 | }}
133 | />
134 |
135 |
136 |