Skip to content

Commit

Permalink
Updated the ruleset for OWASP API Security 2023 edition.
Browse files Browse the repository at this point in the history
  • Loading branch information
philsturgeon committed Feb 5, 2024
1 parent 2e73f8c commit 6b0b9e4
Show file tree
Hide file tree
Showing 37 changed files with 4,811 additions and 1,922 deletions.
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

### 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`.
- 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.
20 changes: 9 additions & 11 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,16 +41,12 @@ 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)).
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

0 comments on commit 6b0b9e4

Please sign in to comment.