Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add example of XSS vulnerability #24

Merged
merged 26 commits into from
Oct 26, 2022
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
8609ea8
added an XSS example
dschwarzc Oct 19, 2022
7e99792
Added XSS solution example with markdown
dschwarzc Oct 23, 2022
9956429
Merge branch 'main' of github.com:upleveled/next-js-postgres-broken-s…
karlhorky Oct 24, 2022
99f374e
Revert allowing cookie access from JS
karlhorky Oct 24, 2022
552f4e0
Lock version, run `yarn`
karlhorky Oct 24, 2022
547b398
Move error to CommonContent component
karlhorky Oct 24, 2022
05a59a1
Make example 4 files easier to compare
karlhorky Oct 24, 2022
3e83c88
Add dompurify types, fix import style
karlhorky Oct 25, 2022
976cdcc
Format code, remove extra p element
karlhorky Oct 25, 2022
d3e7721
Merge branch 'main' of github.com:upleveled/next-js-postgres-broken-s…
karlhorky Oct 25, 2022
c8c0d8d
Set NODE_ENV to production
karlhorky Oct 25, 2022
3d5e89f
Move NODE_ENV to only build step
karlhorky Oct 25, 2022
2b7662b
Export null-returning component for Next.js build
karlhorky Oct 25, 2022
b6df284
Add missing ESLint dependencies, fix problems
karlhorky Oct 25, 2022
2cd00ff
Make blog_posts.user_id NOT NULL
karlhorky Oct 25, 2022
24971a9
Improve clarity of naming, prose and types
karlhorky Oct 25, 2022
4a3f632
Use tsconfig from UpLeveled ESLint config
karlhorky Oct 25, 2022
f5fea9d
Add jsx: preserve to tsconfig.json for Next.js
karlhorky Oct 25, 2022
da3dbe4
Make blog post title and content more descriptive
karlhorky Oct 25, 2022
87e7fcf
Add new screenshot and figcaptions to readme
karlhorky Oct 25, 2022
7b9d1e6
Fix paths and naming
karlhorky Oct 25, 2022
758e298
Fix naming and paths for Cross-Site Scripting
karlhorky Oct 25, 2022
2c42e6e
Fix component names
karlhorky Oct 26, 2022
c77f4a7
Fix component names
karlhorky Oct 26, 2022
1ee6254
Improve clarity of comment
karlhorky Oct 26, 2022
b89d8a8
Add type to getServerSideProps return values
karlhorky Oct 26, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/lint-check-types-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ jobs:
- run: yarn build
env:
API_KEY: ${{ secrets.API_KEY }}
NODE_ENV: production
PGHOST: localhost
PGDATABASE: security_vulnerability_examples
PGUSERNAME: security_vulnerability_examples
Expand Down
Binary file added 6-cross-site-scripting.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions database/blogPosts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@ export async function getBlogPosts() {
return blogPosts;
}

export async function getBlogPostById(id: number) {
const [blogPost] = await sql<BlogPost[]>`
SELECT
*
FROM
blog_posts
WHERE
id = ${id}
`;
return blogPost;
}

export async function getPublishedBlogPosts() {
const blogPosts = await sql<BlogPost[]>`
SELECT
Expand Down
2 changes: 1 addition & 1 deletion database/connect.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { config } from 'dotenv-safe';
import postgres from 'postgres';

config();
if (process.env.NODE_ENV !== 'production') config();

// Type needed for the connection function below
declare module globalThis {
Expand Down
15 changes: 10 additions & 5 deletions migrations/001-create-table-users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ const users = [
// id: 1,
username: 'alice',
// password: abc
password_hash:
passwordHash:
'$2b$12$rip3gbockwavRttTaMZa.u5JKY1542MOLBI7YGkRXaj83rtocfl3a',
},
{
// id: 2,
username: 'bob',
// password: def
password_hash:
passwordHash:
'$2b$12$0N14zwm7.gFNB9UriJpo9eHqCBSezv1zdvbLL7ql79KYJM50fvo6q',
},
];
Expand All @@ -26,9 +26,14 @@ export async function up(sql: Sql<Record<string, string>>) {
);
`;

await sql`
INSERT INTO users ${sql(users, 'username', 'password_hash')}
`;
for (const user of users) {
await sql`
INSERT INTO users
(username, password_hash)
VALUES
(${user.username}, ${user.passwordHash})
`;
}
}

export async function down(sql: Sql<Record<string, string>>) {
Expand Down
63 changes: 38 additions & 25 deletions migrations/003-create-table-blog-posts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,52 @@ import { Sql } from 'postgres';
const blogPosts = [
{
title: "Alice's first post (published)",
text_content:
textContent:
"This is Alice's first post. It's published, so this is data that all logged-in users are allowed to view.",
is_published: true,
user_id: 1,
isPublished: true,
userId: 1,
},
{
title: "Alice's second post (unpublished)",
text_content:
textContent:
"This is Alice's second post. It's not published, so this is private data that only Alice should be able to view and edit.",
is_published: false,
user_id: 1,
isPublished: false,
userId: 1,
},
{
title: "Alice's third post (published)",
text_content:
textContent:
"This is Alice's third post. It's published, so this is data that all logged-in users are allowed to view.",
is_published: true,
user_id: 1,
isPublished: true,
userId: 1,
},
{
title: "Bob's first post (unpublished)",
text_content:
textContent:
"This is Bob's first post. It's not published, so this is data only Bob should be able to view and edit.",
is_published: false,
user_id: 2,
isPublished: false,
userId: 2,
},
{
title: "Bob's second post (published)",
text_content:
textContent:
"This is Bob's second post. It's published, so this is data that all logged-in users are allowed to view.",
is_published: true,
user_id: 2,
isPublished: true,
userId: 2,
},
{
title: "Bob's HTML post (published)",
textContent:
'This is Bob\'s blog post using <b>HTML</b> and an image: <img src="x" onerror="alert(\'pwned\')" />',
isPublished: true,
userId: 1,
},
{
title: "Bob's Markdown post (published)",
textContent:
'This is Bob\'s blog post using **Markdown** and an image in HTML: <img src="x" onerror="alert(\'pwned\')" />',
isPublished: true,
userId: 1,
},
];

Expand All @@ -45,19 +59,18 @@ export async function up(sql: Sql<Record<string, string>>) {
title varchar(100) NOT NULL,
text_content varchar(2000) NOT NULL,
is_published boolean NOT NULL DEFAULT false,
user_id integer REFERENCES users (id) ON DELETE CASCADE
user_id integer NOT NULL REFERENCES users (id) ON DELETE CASCADE
)
`;

await sql`
INSERT INTO blog_posts ${sql(
blogPosts,
'title',
'text_content',
'is_published',
'user_id',
)}
`;
for (const blogPost of blogPosts) {
await sql`
INSERT INTO blog_posts
(title, text_content, is_published, user_id)
VALUES
(${blogPost.title}, ${blogPost.textContent}, ${blogPost.isPublished}, ${blogPost.userId})
`;
}
}

export async function down(sql: Sql<Record<string, string>>) {
Expand Down
26 changes: 16 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,53 +14,59 @@
"@types/dotenv-safe": "8.1.2",
"bcrypt": "5.1.0",
"cookie": "0.5.0",
"dompurify": "2.4.0",
"dotenv-cli": "6.0.0",
"dotenv-safe": "8.2.0",
"ley": "0.8.1",
"next": "12.3.1",
"postgres": "3.3.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-markdown": "8.0.3",
"sharp": "0.31.1",
"tsm": "2.2.2"
},
"devDependencies": {
"@babel/eslint-parser": "7.19.1",
"@next/eslint-plugin-next": "12.3.1",
"@ts-safeql/eslint-plugin": "0.0.18",
"@types/bcrypt": "5.0.0",
"@types/node": "18.11.3",
"@types/dompurify": "2.3.4",
"@types/eslint": "8.4.6",
"@types/node": "18.8.5",
"@types/react": "18.0.21",
"@types/react-dom": "18.0.6",
"@typescript-eslint/eslint-plugin": "5.40.1",
"@typescript-eslint/parser": "5.40.1",
"@typescript-eslint/eslint-plugin": "5.40.0",
"@typescript-eslint/parser": "5.40.0",
"@upleveled/eslint-config-upleveled": "3.0.1",
"@upleveled/eslint-plugin-upleveled": "2.1.6",
"eslint": "8.25.0",
"eslint-config-next": "12.3.1",
"eslint-config-react-app": "7.0.1",
"eslint-import-resolver-typescript": "3.5.2",
"eslint-import-resolver-typescript": "3.5.1",
"eslint-plugin-cypress": "2.12.1",
"eslint-plugin-flowtype": "8.0.3",
"eslint-plugin-import": "2.26.0",
"eslint-plugin-jest": "27.1.3",
"eslint-plugin-jest": "27.1.1",
"eslint-plugin-jsx-a11y": "6.6.1",
"eslint-plugin-jsx-expressions": "1.3.1",
"eslint-plugin-react": "7.31.10",
"eslint-plugin-react-hooks": "4.6.0",
"eslint-plugin-security": "1.5.0",
"eslint-plugin-sonarjs": "0.16.0",
"eslint-plugin-testing-library": "5.9.0",
"eslint-plugin-testing-library": "5.7.2",
"eslint-plugin-unicorn": "44.0.2",
"libpg-query": "13.2.5",
"typescript": "4.8.4"
},
"resolutions": {
"@typescript-eslint/eslint-plugin": "5.40.1",
"@typescript-eslint/parser": "5.40.1",
"@typescript-eslint/eslint-plugin": "5.40.0",
"@typescript-eslint/parser": "5.40.0",
"eslint-plugin-import": "2.26.0",
"eslint-plugin-jest": "27.1.3",
"eslint-plugin-jest": "27.1.1",
"eslint-plugin-jsx-a11y": "6.6.1",
"eslint-plugin-react": "7.31.10",
"eslint-plugin-react-hooks": "4.6.0",
"@typescript-eslint/utils": "5.40.1"
"@typescript-eslint/utils": "5.40.0"
}
}
11 changes: 9 additions & 2 deletions pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,19 @@ export default function App({ Component, pageProps }: AppProps) {
Example 4
</LinkIfNotCurrent>
<LinkIfNotCurrent
href="/example-5-secrets-exposure-gssp/vulnerable"
baseHref="/example-5-secrets-exposure-gssp/"
href="/example-5-secrets-exposure/vulnerable"
baseHref="/example-5-secrets-exposure/"
>
Example 5
</LinkIfNotCurrent>

<LinkIfNotCurrent
href="/example-6-cross-site-scripting/vulnerable"
baseHref="/example-6-cross-site-scripting/"
>
Example 6
</LinkIfNotCurrent>

<div style={{ marginLeft: 'auto', display: 'flex', gap: 15 }}>
{!user && (
<>
Expand Down
26 changes: 6 additions & 20 deletions pages/example-4-missing-authorization-gssp/common.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ type Props =
blogPosts: BlogPost[];
};

export function CommonContent() {
export function CommonContent(props: Props) {
return (
<>
<h1>Missing Authorization - getServerSideProps</h1>
Expand Down Expand Up @@ -49,27 +49,13 @@ export function CommonContent() {
</div>

<h2>Unpublished Blog Posts</h2>

{'error' in props && <div style={{ color: 'red' }}>{props.error}</div>}
</>
);
}

export default function Common(props: Props) {
return (
<div>
<CommonContent />

{'error' in props && <div style={{ color: 'red' }}>{props.error}</div>}

{'blogPosts' in props &&
props.blogPosts.map((blogPost) => {
return (
<div key={`blog-post-${blogPost.id}`}>
<h2>{blogPost.title}</h2>
<div>Published: {String(blogPost.isPublished)}</div>
<div>{blogPost.textContent}</div>
</div>
);
})}
</div>
);
// Export component for Next.js page build
export default function Common() {
return null;
}
19 changes: 17 additions & 2 deletions pages/example-4-missing-authorization-gssp/solution-1.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
BlogPost,
getUnpublishedBlogPostsBySessionToken,
} from '../../database/blogPosts';
import Common from './common';
import { CommonContent } from './common';

type Props =
| {
Expand All @@ -14,7 +14,22 @@ type Props =
};

export default function MissingAuthorizationGssp(props: Props) {
return <Common {...props} />;
return (
<div>
<CommonContent {...props} />

{'blogPosts' in props &&
props.blogPosts.map((blogPost) => {
return (
<div key={`blog-post-${blogPost.id}`}>
<h2>{blogPost.title}</h2>
<div>Published: {String(blogPost.isPublished)}</div>
<div>{blogPost.textContent}</div>
</div>
);
})}
</div>
);
}

export async function getServerSideProps(context: GetServerSidePropsContext) {
Expand Down
19 changes: 17 additions & 2 deletions pages/example-4-missing-authorization-gssp/solution-2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
getUnpublishedBlogPostsByUserId,
} from '../../database/blogPosts';
import { getUserByValidSessionToken } from '../../database/users';
import Common from './common';
import { CommonContent } from './common';

type Props =
| {
Expand All @@ -15,7 +15,22 @@ type Props =
};

export default function MissingAuthorizationGssp(props: Props) {
return <Common {...props} />;
return (
<div>
<CommonContent {...props} />

{'blogPosts' in props &&
props.blogPosts.map((blogPost) => {
return (
<div key={`blog-post-${blogPost.id}`}>
<h2>{blogPost.title}</h2>
<div>Published: {String(blogPost.isPublished)}</div>
<div>{blogPost.textContent}</div>
</div>
);
})}
</div>
);
}

export async function getServerSideProps(context: GetServerSidePropsContext) {
Expand Down
19 changes: 17 additions & 2 deletions pages/example-4-missing-authorization-gssp/vulnerable-1.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { BlogPost, getUnpublishedBlogPosts } from '../../database/blogPosts';
import Common from './common';
import { CommonContent } from './common';

type Props =
| {
Expand All @@ -10,7 +10,22 @@ type Props =
};

export default function MissingAuthorizationGssp(props: Props) {
return <Common {...props} />;
return (
<div>
<CommonContent {...props} />

{'blogPosts' in props &&
props.blogPosts.map((blogPost) => {
return (
<div key={`blog-post-${blogPost.id}`}>
<h2>{blogPost.title}</h2>
<div>Published: {String(blogPost.isPublished)}</div>
<div>{blogPost.textContent}</div>
</div>
);
})}
</div>
);
}

export async function getServerSideProps() {
Expand Down
Loading