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

API Security 2023 #51

Merged
merged 4 commits into from
Feb 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
node_modules/
dist/
.tool-versions
50 changes: 50 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.0.0] - 2024-01-23

### Added

- Added `owasp:api2:2023-short-lived-access-tokens` to error on OAuth 2.x flows which do not use a refresh token.
- Added `owasp:api3:2023-no-unevaluatedProperties` (format `oas3_1` only.)
- Added `owasp:api3:2023-constrained-unevaluatedProperties` (format `oas3_1` only.)
- Added `owasp:api5:2023-admin-security-unique`.
- Added `owasp:api7:2023-concerning-url-parameter` to keep an eye out for URLs being passed as parameters and warn about server-side request forgery.
- Added `owasp:api8:2023-no-server-http` which supports `servers` having a `url` which is a relative path.
- Added `owasp:api9:2023-inventory-access` to indicate intended audience of every server.
- Added `owasp:api9:2023-inventory-environment` to declare intended environment for every server.

philsturgeon marked this conversation as resolved.
Show resolved Hide resolved
### Changed

- Deleted `owasp:api2:2023-protection-global-unsafe` as it allowed for unprotected POST, PATCH, PUT, DELETE and that's always going to be an issue. Use the new `owasp:api2:2023-write-restricted` rule which does not allow these operations to ever disable security, or use [Spectral overrides](https://docs.stoplight.io/docs/spectral/e5b9616d6d50c-rulesets) if you have an edge case.
- Renamed `owasp:api2:2019-protection-global-unsafe-strict` to `owasp:api2:2023-write-restricted`.
- Renamed `owasp:api2:2019-protection-global-safe` to `owasp:api2:2023-read-restricted` and increased severity from `info` to `warn`.
- Renamed `owasp:api2:2019-auth-insecure-schemes` to `owasp:api2:2023-auth-insecure-schemes`.
- Renamed `owasp:api2:2019-jwt-best-practices` to `owasp:api2:2023-jwt-best-practices`.
- Renamed `owasp:api2:2019-no-api-keys-in-url` to `owasp:api2:2023-no-api-keys-in-url`.
- Renamed `owasp:api2:2019-no-credentials-in-url` to `owasp:api2:2023-no-credentials-in-url`.
- Renamed `owasp:api2:2019-no-http-basic` to `owasp:api2:2023-no-http-basic`.
- Renamed `owasp:api3:2019-define-error-validation` to `owasp:api8:2023-define-error-validation`.
- Renamed `owasp:api3:2019-define-error-responses-401` to `owasp:api8:2023-define-error-responses-401`.
- Renamed `owasp:api3:2019-define-error-responses-500` to `owasp:api8:2023-define-error-responses-500`.
- Renamed `owasp:api4:2019-rate-limit` to `owasp:api4:2023-rate-limit` and added support for the singular `RateLimit` header in draft-ietf-httpapi-ratelimit-headers-07.
- Renamed `owasp:api4:2019-rate-limit-retry-after` to `owasp:api4:2023-rate-limit-retry-after`.
- Renamed `owasp:api4:2019-rate-limit-responses-429` to `owasp:api4:2023-rate-limit-responses-429`.
- Renamed `owasp:api4:2019-array-limit` to `owasp:api4:2023-array-limit`.
- Renamed `owasp:api4:2019-string-limit` to `owasp:api4:2023-string-limit`.
- Renamed `owasp:api4:2019-string-restricted` to `owasp:api4:2023-string-restricted` and downgraded from `error` to `warn`.
- Renamed `owasp:api4:2019-integer-limit` to `owasp:api4:2023-integer-limit`.
- Renamed `owasp:api4:2019-integer-limit-legacy` to `owasp:api4:2023-integer-limit-legacy`.
- Renamed `owasp:api4:2019-integer-format` to `owasp:api4:2023-integer-format`.
- Renamed `owasp:api6:2019-no-additionalProperties` to `owasp:api3:2023-no-additionalProperties` and restricted rule to only run the `oas3_0` format.
- Renamed `owasp:api6:2019-constrained-additionalProperties` to `owasp:api3:2023-constrained-additionalProperties` and restricted rule to only run the `oas3_0` format.
- Renamed `owasp:api7:2023-security-hosts-https-oas2` to `owasp:api8:2023-no-scheme-http`.
- Renamed `owasp:api7:2023-security-hosts-https-oas3` to `owasp:api8:2023-no-server-http`.

### Removed

- Deleted `owasp:api2:2023-protection-global-unsafe` as it allowed for unprotected POST, PATCH, PUT, DELETE and that's always going to be an issue. Use the new `owasp:api2:2023-write-restricted` rule which does not allow these operations to ever disable security, or use [Spectral overrides](https://docs.stoplight.io/docs/spectral/e5b9616d6d50c-rulesets) if you have an edge case.
22 changes: 10 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@

Scan an [OpenAPI](https://spec.openapis.org/oas/v3.1.0) document to detect security issues. As OpenAPI is only describing the surface level of the API it cannot see what is happening in your code, but it can spot obvious issues and outdated standards being used.

v2.x of this ruleset is based on the [OWASP API Security Top 10 2023 edition](https://owasp.org/API-Security/editions/2023/en/0x00-header/), but if you would like to use the [2019 edition](https://owasp.org/API-Security/editions/2019/en/0x00-header/) please use v1.x.

## Installation

```bash
npm install --save -D @stoplight/spectral-owasp-ruleset
npm install --save -D @stoplight/spectral-owasp-ruleset@^2.0
npm install --save -D @stoplight/spectral-cli
```

Expand Down Expand Up @@ -39,19 +41,15 @@ You should see some output like this:

```
/Users/phil/src/protect-earth-api/api/openapi.yaml
44:17 warning owasp:api3:2019-define-error-responses-400:400 response should be defined.. Missing responses[400] paths./upload.post.responses
44:17 warning owasp:api3:2019-define-error-responses-429:429 response should be defined.. Missing responses[429] paths./upload.post.responses
44:17 warning owasp:api3:2019-define-error-responses-500:500 response should be defined.. Missing responses[500] paths./upload.post.responses
45:15 error owasp:api4:2019-rate-limit All 2XX and 4XX responses should define rate limiting headers. paths./upload.post.responses[201]
47:15 error owasp:api4:2019-rate-limit All 2XX and 4XX responses should define rate limiting headers. paths./upload.post.responses[401]
53:15 error owasp:api4:2019-rate-limit All 2XX and 4XX responses should define rate limiting headers. paths./upload.post.responses[403]
59:15 error owasp:api4:2019-rate-limit All 2XX and 4XX responses should define rate limiting headers. paths./upload.post.responses[409]
65:15 error owasp:api4:2019-rate-limit All 2XX and 4XX responses should define rate limiting headers. paths./upload.post.responses[422]
193:16 information owasp:api2:2019-protection-global-safe This operation is not protected by any security scheme. paths./sites.get.security
210:16 information owasp:api2:2019-protection-global-safe This operation is not protected by any security scheme. paths./species.get.security
4:5 error owasp:api8:2023-inventory-access Declare intended audience of every server by defining servers[0].x-internal as true/false. servers[0]
4:10 error owasp:api8:2023-no-server-http Server URLs must not use http://. https:// is highly recommended. servers[0].url
45:15 error owasp:api4:2023-rate-limit All 2XX and 4XX responses should define rate limiting headers. paths./upload.post.responses[201]
47:15 error owasp:api4:2023-rate-limit All 2XX and 4XX responses should define rate limiting headers. paths./upload.post.responses[401]
93:16 information owasp:api2:2023-read-restricted This operation is not protected by any security scheme. paths./sites.get.security
210:16 information owasp:api2:2023-read-restricted This operation is not protected by any security scheme. paths./species.get.security
```

Now you have some things to work on for your API. Thankfully these are only at the `warning` and `information` severity, and that is not going to [fail continuous integration](https://meta.stoplight.io/docs/spectral/ZG9jOjExNTMyOTAx-continuous-integration) (unless [you want them to](https://meta.stoplight.io/docs/spectral/ZG9jOjI1MTg1-spectral-cli#error-results)).
Now you have some things to work on for your API. Thankfully these are only at the `warning` and `information` severity, and that is not going to [fail continuous integration](https://docs.stoplight.io/docs/spectral/ZG9jOjExNTMyOTAx-continuous-integration) (unless [you want them to](https://docs.stoplight.io/docs/spectral/ZG9jOjI1MTg1-spectral-cli#error-results)).

There are [a bunch of other rulesets](https://github.com/stoplightio/spectral-rulesets) you can use, or use for inspiration for your own rulesets and API Style Guides.

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { DiagnosticSeverity } from "@stoplight/types";
import testRule from "./__helpers__/helper";

testRule("owasp:api1:2019-no-numeric-ids", [
testRule("owasp:api1:2023-no-numeric-ids", [
{
name: "valid case",
name: "valid case: uuid",
document: {
openapi: "3.1.0",
info: { version: "1.0" },
Expand All @@ -29,6 +29,60 @@ testRule("owasp:api1:2019-no-numeric-ids", [
errors: [],
},

{
name: "valid case: ulid",
document: {
openapi: "3.1.0",
info: { version: "1.0" },
paths: {
"/foo/{id}": {
get: {
description: "get",
parameters: [
{
name: "id",
in: "path",
required: true,
schema: {
type: "string",
format: "ulid",
},
},
],
},
},
},
},
errors: [],
},

{
name: "valid case: random",
document: {
openapi: "3.1.0",
info: { version: "1.0" },
paths: {
"/foo/{id}": {
get: {
description: "get",
parameters: [
{
name: "id",
in: "path",
required: true,
schema: {
type: "string",
example: "sfdjkhjk24kd9s",
},
},
],
},
},
},
},
errors: [],
},

{
name: "invalid if its an integer",
document: {
Expand Down Expand Up @@ -88,25 +142,25 @@ testRule("owasp:api1:2019-no-numeric-ids", [
errors: [
{
message:
"OWASP API1:2019 - Use random IDs that cannot be guessed. UUIDs are preferred.",
"Use random IDs that cannot be guessed. UUIDs are preferred but any other random string will do.",
path: ["paths", "/foo/{id}", "get", "parameters", "0", "schema"],
severity: DiagnosticSeverity.Error,
},
{
message:
"OWASP API1:2019 - Use random IDs that cannot be guessed. UUIDs are preferred.",
"Use random IDs that cannot be guessed. UUIDs are preferred but any other random string will do.",
path: ["paths", "/foo/{id}", "get", "parameters", "2", "schema"],
severity: DiagnosticSeverity.Error,
},
{
message:
"OWASP API1:2019 - Use random IDs that cannot be guessed. UUIDs are preferred.",
"Use random IDs that cannot be guessed. UUIDs are preferred but any other random string will do.",
path: ["paths", "/foo/{id}", "get", "parameters", "3", "schema"],
severity: DiagnosticSeverity.Error,
},
{
message:
"OWASP API1:2019 - Use random IDs that cannot be guessed. UUIDs are preferred.",
"Use random IDs that cannot be guessed. UUIDs are preferred but any other random string will do.",
path: ["paths", "/foo/{id}", "get", "parameters", "4", "schema"],
severity: DiagnosticSeverity.Error,
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { DiagnosticSeverity } from "@stoplight/types";
import testRule from "./__helpers__/helper";

testRule("owasp:api2:2019-auth-insecure-schemes", [
testRule("owasp:api2:2023-auth-insecure-schemes", [
{
name: "valid case",
document: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { DiagnosticSeverity } from "@stoplight/types";
import testRule from "./__helpers__/helper";

testRule("owasp:api2:2019-jwt-best-practices", [
testRule("owasp:api2:2023-jwt-best-practices", [
{
name: "valid case",
document: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { DiagnosticSeverity } from "@stoplight/types";
import testRule from "./__helpers__/helper";

testRule("owasp:api2:2019-no-api-keys-in-url", [
testRule("owasp:api2:2023-no-api-keys-in-url", [
{
name: "valid case",
document: {
Expand Down Expand Up @@ -40,13 +40,13 @@ testRule("owasp:api2:2019-no-api-keys-in-url", [
errors: [
{
message:
'ApiKey passed in URL: "query" must not match the pattern "^(path|query)$".',
'API Key passed in URL: "query" must not match the pattern "^(path|query)$".',
path: ["components", "securitySchemes", "API Key in Query", "in"],
severity: DiagnosticSeverity.Error,
},
{
message:
'ApiKey passed in URL: "path" must not match the pattern "^(path|query)$".',
'API Key passed in URL: "path" must not match the pattern "^(path|query)$".',
path: ["components", "securitySchemes", "API Key in Path", "in"],
severity: DiagnosticSeverity.Error,
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { DiagnosticSeverity } from "@stoplight/types";
import testRule from "./__helpers__/helper";

testRule("owasp:api2:2019-no-credentials-in-url", [
testRule("owasp:api2:2023-no-credentials-in-url", [
{
name: "valid case",
document: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { DiagnosticSeverity } from "@stoplight/types";
import testRule from "./__helpers__/helper";

testRule("owasp:api2:2019-no-http-basic", [
testRule("owasp:api2:2023-no-http-basic", [
{
name: "valid case",
document: {
Expand Down Expand Up @@ -36,7 +36,7 @@ testRule("owasp:api2:2019-no-http-basic", [
errors: [
{
message:
"Security scheme uses HTTP Basic. Use a more secure authentication method, like OAuth 2.0.",
"Security scheme uses HTTP Basic. Use a more secure authentication method, like OAuth 2, or OpenID.",
path: ["components", "securitySchemes", "please-hack-me", "scheme"],
severity: DiagnosticSeverity.Error,
},
Expand Down
71 changes: 71 additions & 0 deletions __tests__/owasp-api2-2023-short-lived-tokens.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { DiagnosticSeverity } from "@stoplight/types";
import testRule from "./__helpers__/helper";

const authorizationCodeFlow = {
authorizationUrl: "https://example.com/oauth/authorize",
tokenUrl: "https://example.com/oauth/token",
scopes: {
read_scope: "Read access to the protected resource",
write_scope: "Write access to the protected resource",
},
};

const oauth2SchemeWithRefreshUrl = {
type: "oauth2",
flows: {
authorizationCode: {
...authorizationCodeFlow,
refreshUrl: "https://example.com/oauth/refresh",
},
},
};

const oauth2SchemeWithoutRefreshUrl = {
type: "oauth2",
flows: {
authorizationCode: authorizationCodeFlow,
},
};

testRule("owasp:api2:2023-short-lived-access-tokens", [
{
name: "valid case",
document: {
openapi: "3.1.0",
info: { version: "1.0" },
components: {
securitySchemes: {
oauth2: oauth2SchemeWithRefreshUrl,
},
},
},
errors: [],
},

{
name: "invalid case",
document: {
openapi: "3.1.0",
info: { version: "1.0" },
components: {
securitySchemes: {
oauth2: oauth2SchemeWithoutRefreshUrl,
},
},
},
errors: [
{
message:
"Authentication scheme does not appear to support refresh tokens, meaning access tokens likely do not expire.",
path: [
"components",
"securitySchemes",
"oauth2",
"flows",
"authorizationCode",
],
severity: DiagnosticSeverity.Error,
},
],
},
]);
Loading
Loading