Skip to content

Commit

Permalink
BREAKING: Rewrite to support OWASP API Security 2023 (#51)
Browse files Browse the repository at this point in the history
* Removes duplicated test case

* Move API3 rules that focused on defining 400, 401, 500 responses out into API8.

* add unevaluatedproperties rule for OAS 3.1

* added owasp-api3-2023-constrained-additional/unevaluated tests

* updated year on api1 and api2 unchanged

* renamed api4:2019 to api4:2023 only

* added owasp:api2:2023-write-restricted and owasp:api2:2023-read-restricted

* Fixes #25: adds owasp:api5:2023-admin-security-unique

* fixes #21 and makes no-nimeric-ids support any string

* added support for no-server-http to use relative path.

* partially fixes #52: Require servers use x-internal true/false

to explicitly explain what is public or internal for documentation tools

* fixes #52: Servers, define which environment is the API running in

---------

Co-authored-by: Ricagraca <[email protected]>
  • Loading branch information
philsturgeon and Ricagraca authored Feb 5, 2024
1 parent 2e73f8c commit f7f7e63
Show file tree
Hide file tree
Showing 38 changed files with 4,937 additions and 1,928 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` 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

0 comments on commit f7f7e63

Please sign in to comment.