-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathapi-security.html
More file actions
1135 lines (1054 loc) · 119 KB
/
Copy pathapi-security.html
File metadata and controls
1135 lines (1054 loc) · 119 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="index, follow, max-snippet:-1, max-image-preview:large">
<meta name="description"
content="API security for cloud - OWASP API Security Top 10 (2023), authentication patterns (OAuth, OIDC, JWT, mTLS), authorization (RBAC, ABAC, OPA, Cedar), the…">
<meta name="author" content="Shawn Nunley">
<meta name="keywords"
content="API security, OWASP API Top 10, BOLA, BFLA, BOPLA, OAuth 2.0, OIDC, JWT, mTLS, API gateway, Kong, API Gateway AWS, Apigee, Azure API Management, Cloudflare API Gateway, schema validation, OpenAPI, GraphQL security, gRPC security, SSRF, API discovery, Salt Security, Traceable, Noname, Cequence, Wallarm, 42Crunch, Pynt, StackHawk, webhooks security, API rate limiting">
<title>API Security - A Vendor-Neutral Practitioner's Guide - CSOH</title>
<link rel="canonical" href="https://csoh.org/api-security.html">
<link rel="alternate" hreflang="en-US" href="https://csoh.org/api-security.html">
<link rel="alternate" hreflang="x-default" href="https://csoh.org/api-security.html">
<link rel="alternate" type="application/rss+xml" title="CSOH Cloud Security News" href="/feed.xml">
<link rel="icon" type="image/png" href="/favicon.png">
<link rel="manifest" href="/manifest.json">
<link rel="author" type="text/plain" href="/humans.txt">
<meta name="theme-color" content="#0f172a">
<meta name="apple-mobile-web-app-title" content="CSOH">
<meta property="og:title" content="API Security - A Vendor-Neutral Practitioner's Guide - CSOH">
<meta property="og:description"
content="The OWASP API Top 10, auth patterns, BOLA, rate limiting, schema validation, GraphQL/gRPC risks, SSRF, discovery, runtime defense, and AWS/Azure/GCP…">
<meta property="og:site_name" content="Cloud Security Office Hours">
<meta property="og:locale" content="en_US">
<meta property="og:type" content="article">
<meta property="article:published_time" content="2026-05-17">
<meta property="article:modified_time" content="2026-05-17">
<meta property="article:author" content="Shawn Nunley">
<meta property="article:section" content="Cloud Security Topics">
<meta property="article:tag" content="API security">
<meta property="article:tag" content="OWASP API Top 10">
<meta property="article:tag" content="BOLA">
<meta property="article:tag" content="BFLA">
<meta property="article:tag" content="BOPLA">
<meta property="article:tag" content="OAuth 2.0">
<meta property="article:tag" content="OIDC">
<meta property="article:tag" content="JWT">
<meta property="og:url" content="https://csoh.org/api-security.html">
<meta property="og:image" content="https://csoh.org/img/og/api-security.jpg">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="API Security - A Vendor-Neutral Practitioner's Guide">
<meta name="twitter:description"
content="OWASP API Top 10, auth, BOLA, rate limits, schemas, GraphQL/gRPC, SSRF, discovery, runtime defense, and provider-native tools.">
<meta name="twitter:image" content="https://csoh.org/img/og/api-security.jpg">
<link rel="preconnect" href="https://csoh.kit.com" crossorigin>
<link rel="preconnect" href="https://github.com" crossorigin>
<link rel="dns-prefetch" href="https://github.com">
<link rel="preload" href="/style.css?v=fbfcc2db" as="style"
integrity="sha384-kDAccZlI8MgEpgryDTHyW5u7Qln3ej47mWOIg5CKFain7VrL+XyzcEz+tEzAWYAb">
<link rel="stylesheet" href="/style.css?v=fbfcc2db"
integrity="sha384-kDAccZlI8MgEpgryDTHyW5u7Qln3ej47mWOIg5CKFain7VrL+XyzcEz+tEzAWYAb">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Article",
"headline": "API Security - A Vendor-Neutral Practitioner's Guide",
"description": "A vendor-neutral practitioner's guide to API security - the OWASP API Top 10, auth and authorization patterns, the BOLA problem, rate limiting, gateways, schema validation, GraphQL and gRPC specifics, SSRF prevention, API discovery, runtime defense platforms, testing, and AWS/Azure/GCP native tools.",
"author": {
"@type": "Person",
"@id": "https://csoh.org/about-shawn-nunley.html#shawn-nunley",
"name": "Shawn Nunley",
"url": "https://csoh.org/about-shawn-nunley.html"
},
"publisher": {
"@type": "Organization",
"name": "Cloud Security Office Hours",
"logo": {
"@type": "ImageObject",
"url": "https://csoh.org/banner.png"
}
},
"datePublished": "2026-05-17",
"dateModified": "2026-05-17",
"mainEntityOfPage": "https://csoh.org/api-security.html",
"speakable": {
"@type": "SpeakableSpecification",
"cssSelector": ["h1", "main p:first-of-type"]
},
"image": "https://csoh.org/banner.png"
}
</script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{"@type": "ListItem", "position": 1, "name": "Home", "item": "https://csoh.org/"},
{"@type": "ListItem", "position": 2, "name": "API Security", "item": "https://csoh.org/api-security.html"}
]
}
</script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "Do I need an API security platform if I already have a WAF?",
"acceptedAnswer": {
"@type": "Answer",
"text": "A WAF and an API security platform overlap on the surface and disagree underneath. A WAF inspects request payloads against signature and rule sets - SQLi, XSS, command injection, common bot patterns. It does not know what a logical user-id is, what 'order #1247 belongs to user A' means, or that the same authenticated user just enumerated 8,000 customer accounts in two minutes. The OWASP API Top 10 is dominated by business-logic failures (BOLA, BFLA, BOPLA, unrestricted resource consumption) that look like perfectly legitimate traffic to a WAF. If you operate a non-trivial API, you generally need both: the WAF for layer-7 noise, the API security platform for the business-logic and inventory problems."
}
},
{
"@type": "Question",
"name": "How is API authentication different from web-app authentication?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Web app auth typically lands on a session cookie set after a login form; the browser carries the cookie automatically; CSRF protections and SameSite cookies manage cross-origin risk. API auth has to work for clients that aren't browsers - mobile apps, service-to-service calls, partner integrations, CI runners, IoT devices - so the credential is usually a bearer token (OAuth access token, JWT) or mutual TLS, transported in an Authorization header rather than a cookie. The hard parts shift: token issuance, scope design, key rotation, signature validation, and replay protection become the dominant concerns, with no browser-supplied default in your favor."
}
},
{
"@type": "Question",
"name": "GraphQL vs REST - which is more secure?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Neither is inherently more secure; they fail in different shapes. REST's surface is many endpoints with structured per-endpoint controls - BOLA, BFLA, and over-fetching dominate. GraphQL's surface is one endpoint with arbitrary query shapes, so authorization has to be enforced at every field resolver, and the attacker can compose expensive queries (depth, breadth, batching) that a REST consumer would have to make as separate calls. The pragmatic answer: GraphQL needs depth/complexity limits, persisted queries in production, introspection disabled in prod, and field-level authorization. REST needs object-level checks on every read and consistent schema validation. Both need rate limiting tuned to their query economics."
}
},
{
"@type": "Question",
"name": "What is BOLA and why does almost every API have it?",
"acceptedAnswer": {
"@type": "Answer",
"text": "BOLA - Broken Object Level Authorization - is the bug where an endpoint correctly authenticates the caller and confirms they can call this kind of endpoint, but does not check whether the specific object referenced in the request belongs to that caller. /api/orders/1247 returns order 1247 regardless of whether the authenticated user owns it. The reason it's nearly universal: object ownership lives in business data, not in the auth middleware, so the check has to be written into every handler - and one missed handler is a complete data-disclosure bug. The defenses are architectural (scope all queries through an authorization layer that joins user-id with object-id) and detective (log every cross-user object access and alert on patterns)."
}
},
{
"@type": "Question",
"name": "Should I disable GraphQL introspection in production?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Yes for almost every internet-facing GraphQL API. Introspection lets a client query the entire schema - types, fields, arguments, deprecations - which is exactly the recon a credentialed or unauthenticated attacker uses to plan further queries. Keep introspection on in development; disable or scope it (behind an internal-only header, a feature flag, or auth role) in production. The same goes for gRPC reflection."
}
},
{
"@type": "Question",
"name": "Is mTLS overkill for service-to-service APIs?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Not in 2026. For east-west service-to-service traffic in a Kubernetes or service-mesh environment, mTLS is now the default - Istio, Linkerd, Consul, and the managed meshes all bring it without per-app code changes. For north-south (gateway to backend, partner integrations), mTLS is heavier to operate (cert lifecycle, partner onboarding) but pays back when the data is sensitive enough that 'a leaked bearer token is a breach.' The decision is usually: mesh inside, OAuth/OIDC at the edge, optional mTLS at the edge for high-trust partners."
}
},
{
"@type": "Question",
"name": "How do I find APIs I don't know exist?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Shadow APIs - endpoints that exist in production but aren't in the inventory - are the dominant API governance problem. Discovery happens three ways: from traffic (mirror or sample gateway logs, parse for distinct paths, methods, and authentication shape), from code (scan repos for route definitions in known frameworks), and from the cloud (enumerate API Gateway / APIM / Apigee resources across every account). API security platforms (Salt, Traceable, Noname/Akamai, Cequence, Wallarm) build inventory from passive traffic; CSPMs increasingly enumerate the cloud-managed APIs. The maturity goal is one inventory that combines all three sources, deduplicated, with each entry mapped to an owning team."
}
}
]
}
</script>
<noscript><style>.js-enabled header nav{display:block !important}</style></noscript>
</head>
<body class="js-enabled">
<a href="#main-content" class="skip-link">Skip to main content</a>
<header>
<div class="header-content">
<a href="index.html" class="logo-link">
<svg class="logo-mark" viewBox="0 0 32 32" width="32" height="32" role="img" aria-label="CSOH logo"><defs><linearGradient id="lm-cloud" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="#7dd3fc"/><stop offset="100%" stop-color="#0284c7"/></linearGradient></defs><path d="M7 19 a4 4 0 0 1 4-4 a6 6 0 0 1 11 0 a3.5 3.5 0 0 1 3.5 3.5 a3.5 3.5 0 0 1 -3.5 3.5 H8 a3.5 3.5 0 0 1 -1-7 z" fill="url(#lm-cloud)"/><path d="M11 13 L21 13 L21 18 a5 5.5 0 0 1 -5 5 a5 5.5 0 0 1 -5 -5 z" fill="#f59e0b"/><path d="M13.5 16.7 L15.3 18.4 L18.6 15" stroke="#fff" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
<div class="logo">
<div class="logo-title">CSOH</div>
<p>Cloud Security Office Hours</p>
</div>
</a>
<button class="hamburger" aria-label="Toggle navigation" aria-expanded="false">☰</button>
<button class="theme-toggle" aria-label="Switch to dark mode">🌙</button>
<nav>
<ul>
<li class="has-dropdown has-mega">
<button class="dropdown-toggle active" aria-expanded="true" aria-haspopup="true">Learn <span class="caret" aria-hidden="true">▾</span></button>
<div class="dropdown-menu mega-menu mega-5col">
<div class="mega-col">
<span class="mega-heading">Foundations</span>
<ul>
<li><a href="what-is-cloud-security.html">What is Cloud Security?</a></li>
<li><a href="shared-responsibility-model.html">Shared Responsibility</a></li>
<li><a href="cspm-vs-cnapp.html">CSPM vs CNAPP</a></li>
<li><a href="cloud-security-best-practices.html">Best Practices</a></li>
<li><a href="vendor-landscape.html">Vendor Landscape</a></li>
<li><a href="glossary.html">Glossary</a></li>
<li><a href="faq.html">FAQ</a></li>
</ul>
</div>
<div class="mega-col">
<span class="mega-heading">Workloads & Platform</span>
<ul>
<li><a href="containers.html">Containers</a></li>
<li><a href="kubernetes.html">Kubernetes</a></li>
<li><a href="serverless.html">Serverless</a></li>
<li><a href="service-mesh-security.html">Service Mesh</a></li>
<li><a href="ci-cd.html">CI/CD</a></li>
<li><a href="landing-zones.html">Landing Zones</a></li>
</ul>
</div>
<div class="mega-col">
<span class="mega-heading">Security Domains</span>
<ul>
<li><a href="iam.html">IAM & Identity</a></li>
<li><a href="zero-trust.html">Zero Trust</a></li>
<li><a href="network-security.html">Network Security</a></li>
<li><a href="data-security.html">Data Security & KMS</a></li>
<li><a href="vulnerability-management.html">Vulnerability Management</a></li>
<li><a href="api-security.html" aria-current="page">API Security</a></li>
<li><a href="saas-security.html">SaaS Security (SSPM)</a></li>
</ul>
</div>
<div class="mega-col">
<span class="mega-heading">Governance & AI</span>
<ul>
<li><a href="backup-dr.html">Backup, DR & Ransomware</a></li>
<li><a href="threat-modeling.html">Threat Modeling</a></li>
<li><a href="grc.html">GRC</a></li>
<li><a href="compliance-frameworks.html">Compliance Frameworks</a></li>
<li><a href="ai-learning.html">AI Learning</a></li>
<li><a href="ai-ml-security.html">AI/ML Security</a></li>
</ul>
</div>
<div class="mega-col">
<span class="mega-heading">Build It</span>
<ul>
<li class="mega-featured"><a href="cloud-deployment.html">Multi-Cloud Secure Deploy<span class="mega-tag">AWS · GCP · Azure, end to end</span></a></li>
<li><a href="github-actions.html">GitHub Actions</a></li>
<li><a href="terraform.html">Terraform</a></li>
<li><a href="version-control.html">Git & Version Control</a></li>
</ul>
</div>
</div>
</li>
<li><a href="resources.html">Resources</a></li>
<li class="has-dropdown">
<button class="dropdown-toggle" aria-expanded="false" aria-haspopup="true">By Cloud <span class="caret" aria-hidden="true">▾</span></button>
<ul class="dropdown-menu">
<li><a href="aws-security.html">AWS Security</a></li>
<li><a href="azure-security.html">Azure Security</a></li>
<li><a href="gcp-security.html">GCP Security</a></li>
<li><a href="cloud-security-comparison.html">AWS vs Azure vs GCP</a></li>
</ul>
</li>
<li class="has-dropdown">
<button class="dropdown-toggle" aria-expanded="false" aria-haspopup="true">Threat Intel <span class="caret" aria-hidden="true">▾</span></button>
<ul class="dropdown-menu">
<li><a href="news.html">News</a></li>
<li><a href="threat-research.html">Threat Research</a></li>
<li><a href="breach-timeline.html">Breach Kill Chains</a></li>
<li><a href="cloud-soc.html">Cloud SOC</a></li>
<li><a href="detection-engineering.html">Detection Engineering</a></li>
<li><a href="incident-response.html">Incident Response</a></li>
<li><a href="cloud-pentesting.html">Cloud Pentesting</a></li>
<li><a href="ctfs.html">CTFs</a></li>
</ul>
</li>
<li class="has-dropdown has-mega">
<button class="dropdown-toggle" aria-expanded="false" aria-haspopup="true">Careers <span class="caret" aria-hidden="true">▾</span></button>
<div class="dropdown-menu mega-menu mega-3col">
<div class="mega-col">
<span class="mega-heading">Getting Started</span>
<ul>
<li><a href="cloud-security-careers.html">Careers Overview</a></li>
<li><a href="learning-path.html">Learning Path</a></li>
<li><a href="help-desk-to-cloud-security.html">Help Desk → Cloud Security</a></li>
<li><a href="cloud-security-certifications.html">Certifications</a></li>
<li><a href="cloud-security-degree-programs.html">Degree Programs</a></li>
<li><a href="cloud-security-home-lab.html">Home Lab</a></li>
<li><a href="cloud-security-portfolio-projects.html">Portfolio Projects</a></li>
<li><a href="cloud-security-reading-list.html">Reading List</a></li>
<li><a href="mentorship.html">Mentorship</a></li>
</ul>
</div>
<div class="mega-col">
<span class="mega-heading">Engineering Roles</span>
<ul>
<li><a href="cloud-security-engineer.html">Cloud Security Engineer</a></li>
<li><a href="cloud-security-architect.html">Security Architect</a></li>
<li><a href="cloud-security-platform-engineer.html">Platform / Security SRE</a></li>
<li><a href="cloud-security-appsec-engineer.html">AppSec / IaC Engineer</a></li>
<li><a href="cloud-security-cnapp-analyst.html">CSPM / CNAPP Analyst</a></li>
<li><a href="cloud-security-iam-architect.html">IAM / Identity Architect</a></li>
</ul>
</div>
<div class="mega-col">
<span class="mega-heading">Specialist & Field Roles</span>
<ul>
<li><a href="cloud-security-detection-engineer.html">Detection Engineer</a></li>
<li><a href="cloud-security-incident-responder.html">Incident Responder (DFIR)</a></li>
<li><a href="cloud-security-penetration-tester.html">Penetration Tester / Red Team</a></li>
<li><a href="cloud-security-grc-engineer.html">GRC / Compliance Engineer</a></li>
<li><a href="cloud-security-sales-engineer.html">Sales Engineer</a></li>
<li><a href="cloud-security-customer-success-engineer.html">Customer Success Engineer</a></li>
</ul>
</div>
</div>
</li>
<li class="has-dropdown has-mega">
<button class="dropdown-toggle" aria-expanded="false" aria-haspopup="true">Community <span class="caret" aria-hidden="true">▾</span></button>
<div class="dropdown-menu mega-menu mega-2col">
<div class="mega-col">
<span class="mega-heading">Live</span>
<ul>
<li><a href="sessions.html">Friday Zoom Sessions</a></li>
<li><a href="community.html">Community & Signal</a></li>
<li><a href="mentorship.html">Mentorship</a></li>
<li><a href="conferences.html">Conferences</a></li>
</ul>
</div>
<div class="mega-col">
<span class="mega-heading">Archive</span>
<ul>
<li><a href="meetings.html">Meeting Recaps</a></li>
<li><a href="presentations.html">Presentations</a></li>
<li><a href="chat-resources.html">Chat Resources</a></li>
</ul>
</div>
</div>
</li>
<li class="nav-cta-item"><a href="https://csoh.kit.com/39feb4f397" class="nav-cta" target="_blank" rel="noopener noreferrer">Join Friday Zoom →</a></li>
</ul>
</nav>
</div>
</header>
<nav class="breadcrumb-nav" aria-label="Breadcrumb">
<ol>
<li><a href="index.html">Home</a></li>
<li><span aria-current="page">API Security</span></li>
</ol>
</nav>
<section class="hero hero--illustrated">
<div class="hero-inner">
<div class="hero-text">
<h1>API Security</h1>
<p>APIs are now the dominant <a class="glossary-link" href="glossary.html#term-attack-surface">attack surface</a> of the <a class="glossary-link" href="glossary.html#term-cloud-native">cloud-native</a> enterprise - every mobile client, every partner integration, every microservice, every webhook is an API call. This is the vendor-neutral practitioner's guide: the <a class="glossary-link" href="glossary.html#term-owasp">OWASP</a> API Top 10 walked through one item at a time, authentication and authorization patterns, the <a class="glossary-link" href="glossary.html#term-bola">BOLA</a> problem, rate limiting, the API-gateway landscape, schema validation, <a class="glossary-link" href="glossary.html#term-graphql">GraphQL</a> and <a class="glossary-link" href="glossary.html#term-grpc">gRPC</a> specifics, <a class="glossary-link" href="glossary.html#term-ssrf">SSRF</a>, discovery and inventory, runtime defense platforms, testing, and the AWS / Azure / GCP native tooling - followed by maturity stages and the pitfalls that show up in nearly every program.</p>
<div class="cta-buttons">
<a href="#owasp" class="btn btn-primary">OWASP API Top 10 →</a>
<a href="#comparison" class="btn btn-secondary">AWS / Azure / GCP comparison</a>
</div>
</div>
<div class="hero-illustration" aria-hidden="true">
<svg viewBox="0 0 600 480" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Illustration of an API gateway as a checkpoint between clients on the left and microservices on the right, with three stacked layers labeled authentication, rate limiting, and schema validation" width="600" height="480">
<defs>
<linearGradient id="api-sky" x1="0%" y1="0%" x2="0%" y2="100%"><stop offset="0%" stop-color="#bae6fd"/><stop offset="100%" stop-color="#38bdf8"/></linearGradient>
<linearGradient id="api-gate" x1="0%" y1="0%" x2="0%" y2="100%"><stop offset="0%" stop-color="#1e3a8a"/><stop offset="100%" stop-color="#0c4a6e"/></linearGradient>
<linearGradient id="api-auth" x1="0%" y1="0%" x2="1" y2="0"><stop offset="0%" stop-color="#f59e0b"/><stop offset="100%" stop-color="#fbbf24"/></linearGradient>
<linearGradient id="api-rate" x1="0%" y1="0%" x2="1" y2="0"><stop offset="0%" stop-color="#0ea5e9"/><stop offset="100%" stop-color="#38bdf8"/></linearGradient>
<linearGradient id="api-schema" x1="0%" y1="0%" x2="1" y2="0"><stop offset="0%" stop-color="#7c3aed"/><stop offset="100%" stop-color="#a78bfa"/></linearGradient>
<radialGradient id="api-halo" cx="50%" cy="50%" r="50%"><stop offset="0%" stop-color="#7dd3fc" stop-opacity="0.35"/><stop offset="100%" stop-color="#7dd3fc" stop-opacity="0"/></radialGradient>
</defs>
<rect x="0" y="0" width="600" height="480" fill="url(#api-sky)" opacity="0.15"/>
<circle cx="300" cy="240" r="220" fill="url(#api-halo)"/>
<!-- Clients (left side) -->
<g>
<rect x="30" y="80" width="100" height="50" rx="6" fill="#0c4a6e" stroke="#7dd3fc" stroke-width="1.5"/>
<text x="80" y="100" text-anchor="middle" fill="#e0f2fe" font-size="11" font-weight="700">Browser</text>
<text x="80" y="118" text-anchor="middle" fill="#bae6fd" font-size="9">SPA / cookies</text>
<rect x="30" y="160" width="100" height="50" rx="6" fill="#0c4a6e" stroke="#7dd3fc" stroke-width="1.5"/>
<text x="80" y="180" text-anchor="middle" fill="#e0f2fe" font-size="11" font-weight="700">Mobile app</text>
<text x="80" y="198" text-anchor="middle" fill="#bae6fd" font-size="9">OAuth bearer</text>
<rect x="30" y="240" width="100" height="50" rx="6" fill="#0c4a6e" stroke="#7dd3fc" stroke-width="1.5"/>
<text x="80" y="260" text-anchor="middle" fill="#e0f2fe" font-size="11" font-weight="700">Partner</text>
<text x="80" y="278" text-anchor="middle" fill="#bae6fd" font-size="9"><a class="glossary-link" href="glossary.html#term-mtls">mTLS</a> / API key</text>
<rect x="30" y="320" width="100" height="50" rx="6" fill="#0c4a6e" stroke="#7dd3fc" stroke-width="1.5"/>
<text x="80" y="340" text-anchor="middle" fill="#e0f2fe" font-size="11" font-weight="700">CLI / <a class="glossary-link" href="glossary.html#term-ci-cd">CI</a></text>
<text x="80" y="358" text-anchor="middle" fill="#bae6fd" font-size="9">service token</text>
</g>
<!-- Arrows from clients to gateway -->
<g stroke="#0ea5e9" stroke-width="1.5" fill="none" opacity="0.6" stroke-dasharray="4 3">
<line x1="130" y1="105" x2="220" y2="200"/>
<line x1="130" y1="185" x2="220" y2="220"/>
<line x1="130" y1="265" x2="220" y2="240"/>
<line x1="130" y1="345" x2="220" y2="260"/>
</g>
<!-- API Gateway (center checkpoint) -->
<g>
<rect x="220" y="80" width="160" height="320" rx="10" fill="url(#api-gate)" stroke="#fbbf24" stroke-width="2"/>
<text x="300" y="110" text-anchor="middle" fill="#fbbf24" font-size="14" font-weight="800">API Gateway</text>
<text x="300" y="128" text-anchor="middle" fill="#fde68a" font-size="10">north-south checkpoint</text>
<!-- Auth layer -->
<rect x="232" y="148" width="136" height="56" rx="6" fill="url(#api-auth)" stroke="#fff" stroke-width="1"/>
<text x="300" y="172" text-anchor="middle" fill="#7c2d12" font-size="11" font-weight="800">AUTHENTICATION</text>
<text x="300" y="190" text-anchor="middle" fill="#7c2d12" font-size="9">OAuth · <a class="glossary-link" href="glossary.html#term-oidc">OIDC</a> · <a class="glossary-link" href="glossary.html#term-jwt">JWT</a> · mTLS</text>
<!-- Rate limit layer -->
<rect x="232" y="218" width="136" height="56" rx="6" fill="url(#api-rate)" stroke="#fff" stroke-width="1"/>
<text x="300" y="242" text-anchor="middle" fill="#0c4a6e" font-size="11" font-weight="800">RATE LIMITING</text>
<text x="300" y="260" text-anchor="middle" fill="#0c4a6e" font-size="9">per-user · per-IP · per-route</text>
<!-- Schema layer -->
<rect x="232" y="288" width="136" height="56" rx="6" fill="url(#api-schema)" stroke="#fff" stroke-width="1"/>
<text x="300" y="312" text-anchor="middle" fill="#1e1b4b" font-size="11" font-weight="800">SCHEMA VALIDATION</text>
<text x="300" y="330" text-anchor="middle" fill="#1e1b4b" font-size="9">OpenAPI · JSON Schema</text>
<!-- Reject indicator -->
<rect x="232" y="358" width="136" height="32" rx="6" fill="#0d1424" stroke="#ef4444" stroke-width="1.5"/>
<text x="300" y="378" text-anchor="middle" fill="#fca5a5" font-size="10" font-weight="700">Deny if any layer fails</text>
</g>
<!-- Arrows from gateway to services -->
<g stroke="#22c55e" stroke-width="1.5" fill="none" opacity="0.7">
<line x1="380" y1="180" x2="460" y2="120"/>
<polygon points="460,120 452,116 454,124" fill="#22c55e"/>
<line x1="380" y1="220" x2="460" y2="200"/>
<polygon points="460,200 452,196 454,204" fill="#22c55e"/>
<line x1="380" y1="260" x2="460" y2="280"/>
<polygon points="460,280 452,276 454,284" fill="#22c55e"/>
<line x1="380" y1="300" x2="460" y2="360"/>
<polygon points="460,360 452,356 454,364" fill="#22c55e"/>
</g>
<!-- Microservices (right side) -->
<g>
<rect x="460" y="100" width="110" height="44" rx="6" fill="#0d1424" stroke="#34d399" stroke-width="1.5"/>
<text x="515" y="118" text-anchor="middle" fill="#a7f3d0" font-size="10" font-weight="700">orders-svc</text>
<text x="515" y="134" text-anchor="middle" fill="#6ee7b7" font-size="8">REST</text>
<rect x="460" y="180" width="110" height="44" rx="6" fill="#0d1424" stroke="#34d399" stroke-width="1.5"/>
<text x="515" y="198" text-anchor="middle" fill="#a7f3d0" font-size="10" font-weight="700">users-svc</text>
<text x="515" y="214" text-anchor="middle" fill="#6ee7b7" font-size="8">GraphQL</text>
<rect x="460" y="260" width="110" height="44" rx="6" fill="#0d1424" stroke="#34d399" stroke-width="1.5"/>
<text x="515" y="278" text-anchor="middle" fill="#a7f3d0" font-size="10" font-weight="700">billing-svc</text>
<text x="515" y="294" text-anchor="middle" fill="#6ee7b7" font-size="8">gRPC</text>
<rect x="460" y="340" width="110" height="44" rx="6" fill="#0d1424" stroke="#34d399" stroke-width="1.5"/>
<text x="515" y="358" text-anchor="middle" fill="#a7f3d0" font-size="10" font-weight="700">events-svc</text>
<text x="515" y="374" text-anchor="middle" fill="#6ee7b7" font-size="8">webhooks</text>
</g>
<!-- Title at bottom -->
<text x="300" y="440" text-anchor="middle" fill="#0c4a6e" font-size="12" font-weight="700">Untrusted clients · trusted services · everything in between is the gateway's job</text>
</svg>
</div>
</div>
</section>
<main class="container contribute-article" id="main-content">
<figure class="page-photo">
<picture>
<source srcset="/img/photos/data-cables.webp" type="image/webp">
<img src="/img/photos/data-cables.jpg" alt="Bundles of network cables converging into a switch, illustrating API traffic flow" width="1880" height="1253" loading="lazy" decoding="async">
</picture>
<figcaption>Photo by Brett Sayles on Pexels</figcaption>
</figure>
<p class="page-meta"><time datetime="2026-05-17">Last updated <strong>2026-05-17</strong></time> · <a href="about-shawn-nunley.html" rel="author">By Shawn Nunley</a> · Vendor-neutral · <a href="https://github.com/CloudSecurityOfficeHours/csoh.org/blob/main/api-security.html" target="_blank" rel="noopener noreferrer">View source on GitHub</a></p>
<div class="info-box--blue">
<p><strong>The 30-second version:</strong> APIs are the dominant attack surface of any modern cloud-native business - the OWASP API Top 10 exists because the bugs are different from web-app bugs. WAFs, CSP headers, and code-scanning miss most of them because the worst API issues (BOLA, <a class="glossary-link" href="glossary.html#term-bfla">BFLA</a>, <a class="glossary-link" href="glossary.html#term-bopla">BOPLA</a>, unrestricted resource consumption) are <em>business-logic</em> failures that look like legitimate traffic.</p>
<p style="margin-top:0.75rem;">The pragmatic stack: an <strong>API gateway</strong> that enforces auth, rate limiting, and schema validation; <strong><a class="glossary-link" href="glossary.html#term-oauth-2-0">OAuth 2.0</a> / OIDC</strong> for delegated auth, with <strong>JWT</strong> validation done carefully; <strong>fine-grained authorization</strong> per object (BOLA-by-design); <strong>schema-first</strong> APIs validated at the edge; <strong>discovery tooling</strong> so shadow APIs surface; and a runtime <strong>API security platform</strong> that watches the traffic for the business-logic abuses signatures can't catch.</p>
</div>
<div class="toc">
<h2>On this page</h2>
<ol>
<li><a href="#discipline">Why API security is its own discipline now</a></li>
<li><a href="#owasp">The OWASP API Security Top 10 (2023)</a></li>
<li><a href="#authn">Authentication patterns</a></li>
<li><a href="#authz">Authorization patterns</a></li>
<li><a href="#bola">The BOLA problem</a></li>
<li><a href="#rate-limit">Rate limiting</a></li>
<li><a href="#gateways">API gateway landscape</a></li>
<li><a href="#schema">Schema validation</a></li>
<li><a href="#graphql">GraphQL-specific risks</a></li>
<li><a href="#grpc">gRPC-specific concerns</a></li>
<li><a href="#ssrf">SSRF prevention in APIs</a></li>
<li><a href="#discovery">Discovery & inventory</a></li>
<li><a href="#runtime">Runtime API security platforms</a></li>
<li><a href="#testing">API testing</a></li>
<li><a href="#webhooks">Webhooks security</a></li>
<li><a href="#third-party">Third-party API consumption</a></li>
<li><a href="#mesh">APIs in the service mesh</a></li>
<li><a href="#ai-apis">Securing AI, LLM, and agent APIs</a></li>
<li><a href="#comparison">AWS, Azure, and GCP side-by-side</a></li>
<li><a href="#maturity">Maturity stages</a></li>
<li><a href="#pitfalls">Common pitfalls</a></li>
<li><a href="#further">Further reading</a></li>
<li><a href="#faq">FAQ</a></li>
<li><a href="#next">Where next</a></li>
</ol>
</div>
<section id="discipline">
<h2>Why API security is its own discipline now</h2>
<p>The web in 2010 was rendered HTML; the web in 2026 is the JSON behind the rendered HTML, plus mobile clients, plus B2B integrations, plus webhook callbacks, plus the service-to-service traffic inside your own cluster. The API is the product surface, and it's the attack surface.</p>
<p>The shift created bugs the application-security toolchain didn't anticipate. A <a href="https://modsecurity.org/" target="_blank" rel="noopener noreferrer">ModSecurity</a> rule set is unhelpful against an authenticated user asking for someone else's order. A Content-Security-Policy header doesn't apply to a mobile client or a partner pulling from a webhook. <a class="glossary-link" href="glossary.html#term-sast">SAST</a> tooling that grep'd for SQL injection patterns doesn't catch that <code>GET /api/orders/{id}</code> never re-checks ownership.</p>
<p>The category is now formal enough to have its own OWASP top-10 (2019 first edition, <a href="https://owasp.org/API-Security/editions/2023/en/0x00-header/" target="_blank" rel="noopener noreferrer">2023 the current</a>), its own analyst category (Gartner's "API security" was carved out of <a class="glossary-link" href="glossary.html#term-waf">WAF</a> in 2021), and a dedicated vendor landscape (Salt, Traceable, Noname/Akamai, Cequence, Wallarm, 42Crunch). If you operate non-trivial APIs and you're treating API security as "the WAF will catch it," you are operating on a 2015 model of the web.</p>
<h3>What changed</h3>
<ul>
<li><strong>Authorization is per-object, not per-endpoint.</strong> "Can this user call /orders?" is the easy question; "can this user see <em>this</em> order?" is the question modern auth middleware doesn't answer by default.</li>
<li><strong>Authentication moved out of the cookie jar.</strong> Bearer tokens, JWTs, mTLS - each with its own pitfalls and none with browser-supplied defaults.</li>
<li><strong>Traffic shapes are diverse.</strong> A REST endpoint, a GraphQL query, a gRPC stream, and a webhook delivery look nothing alike to a WAF.</li>
<li><strong>The inventory problem is real.</strong> Most non-trivial orgs cannot enumerate every API they expose, what data it returns, who owns it, and whether it's still <a class="glossary-link" href="glossary.html#term-encryption-at-rest">in use</a>.</li>
<li><strong>Business-logic abuse looks legitimate.</strong> The attacker is logged in. Their requests pass the schema. The bug is in what the endpoint <em>does</em> with the request - and only behavioral analytics catches it.</li>
</ul>
</section>
<section id="owasp">
<h2>The OWASP API Security Top 10 (2023)</h2>
<p>The OWASP API Top 10 is the de-facto checklist. The 2023 edition reflects a maturity shift from 2019 - BOLA still dominates, but the list now reflects how often authorization, not authentication, is the failure mode. Walking through each:</p>
<h3>API1:2023 - Broken Object Level Authorization (BOLA)</h3>
<p>The single most common API bug. The endpoint authenticates the caller and confirms they can call this kind of endpoint, but does not check whether the specific object referenced in the request belongs to that caller. <code>GET /api/orders/1247</code> returns order 1247 regardless of who owns it. See <a href="#bola">the BOLA section</a> for the architectural defense.</p>
<h3>API2:2023 - Broken Authentication</h3>
<p>Tokens that can be forged, replayed, brute-forced, or stolen via misconfigured flows. The dominant cases: JWTs accepted without signature verification, OAuth flows that don't validate the audience, password-reset flows that leak tokens, and authentication endpoints without rate limiting. Treat the auth surface as critical; subject it to dedicated review and rate-limit it harder than anything else.</p>
<h3>API3:2023 - Broken Object Property Level Authorization (BOPLA)</h3>
<p>The endpoint correctly checks that the caller owns the object, but does not check which <em>properties</em> they can read or write. Mass-assignment (the caller PATCHes <code>{is_admin: true}</code> and the server accepts it) is the write side; over-fetching (the caller GETs the object and the response includes fields they shouldn't see) is the read side. Defended by allowlisting properties in DTOs, never spreading user input into the model, and never returning the model directly.</p>
<h3>API4:2023 - Unrestricted Resource Consumption</h3>
<p>The endpoint accepts a request that costs the server far more than the caller. Examples: pagination with no max page-size, file uploads with no size limit, search queries with no time budget, deeply nested GraphQL queries, batch endpoints with no batch-size cap. Defended by per-endpoint quotas, per-user budgets, query-complexity limits, and circuit breakers on downstream calls. The relevant cloud-cost angle: an attacker exploiting this against your <a class="glossary-link" href="glossary.html#term-serverless">serverless</a> API may not breach data but will breach your AWS bill.</p>
<h3>API5:2023 - Broken Function Level Authorization (BFLA)</h3>
<p>The same shape as BOLA but for functions rather than objects: the caller can invoke an admin-only function because the function-level check is missing. <code>POST /api/admin/users/123/delete</code> succeeds for a non-admin caller because the admin role-check is in the UI, not the API. Defended by central authorization middleware that enumerates required permissions per route, with deny-by-default.</p>
<h3>API6:2023 - Unrestricted Access to Sensitive Business Flows</h3>
<p>The endpoint is technically working as designed but is being abused at scale - bulk account creation, ticket scalping, gift-card balance enumeration, scraping product catalogs. The auth and authorization pass; the abuse is in the volume. Defended at the platform layer: behavioral analytics (the API security platforms are heavily focused here), bot management, fraud-detection signals, and stepped-up auth for high-value flows.</p>
<h3>API7:2023 - Server Side Request Forgery (SSRF)</h3>
<p>The API accepts a URL from the client and fetches it on the server - image-import, webhook-test, URL-preview, OAuth-callback features all do this. An attacker supplies a URL pointing at the instance metadata service, an internal admin endpoint, or a cloud-internal API. See <a href="#ssrf">SSRF prevention</a> for the defense pattern.</p>
<h3>API8:2023 - Security Misconfiguration</h3>
<p>The grab-bag: verbose errors revealing stack traces, CORS misconfigurations that allow any origin, missing security headers, default credentials on management endpoints, debug routes left enabled, GraphQL introspection in production, gRPC reflection in production. Defended by baselined gateway configs, a security-config policy that lives in the <a class="glossary-link" href="glossary.html#term-iac">IaC</a>, and <a class="glossary-link" href="glossary.html#term-cspm">CSPM</a>-style scanning of API gateways and APIM resources.</p>
<h3>API9:2023 - Improper Inventory Management</h3>
<p>Old API versions still deployed (v1 of the API was supposed to be off; it isn't), staging environments accessible from the internet, deprecated endpoints with weaker auth still online, and the broader "shadow API" problem of endpoints nobody knows exist. The inventory problem is now a first-class top-10 item - see <a href="#discovery">discovery & inventory</a>.</p>
<h3>API10:2023 - Unsafe Consumption of APIs</h3>
<p>The flip side - your code calls a third-party API and trusts the response without validation, exposing you to vulnerabilities the upstream introduces. Compromised upstream provider returns malicious payloads, or unsafe deserialization on response data. Defended by validating responses the same way you validate requests, timeouts and circuit breakers on outgoing calls, and treating third-party API responses as untrusted input.</p>
</section>
<figure class="page-photo">
<picture>
<source srcset="/img/photos/padlock-hand.webp" type="image/webp">
<img src="/img/photos/padlock-hand.jpg" alt="A hand holding a small padlock against a circuit pattern, symbolic of API authentication and authorization" width="1880" height="1253" loading="lazy" decoding="async">
</picture>
<figcaption>Photo on Pexels - auth is the gate but not the whole story.</figcaption>
</figure>
<section id="authn">
<h2>Authentication patterns</h2>
<p>The credential the client presents and how you validate it. The five patterns you'll encounter:</p>
<h3>API keys</h3>
<p>An opaque string the caller includes in a header. Fine for low-risk, low-value, server-to-server contexts where rotation is procedural and the <a class="glossary-link" href="glossary.html#term-blast-radius">blast radius</a> of a leak is small (read-only public datasets, sandbox environments, "click here for a free key" demo APIs). Not fine for anything that touches user data - there's no user identity, no expiration by default, and no way to scope an individual call. If you use API keys, at minimum: rotate them on a schedule, scope them per-environment, log every use, and rate-limit them aggressively.</p>
<h3>OAuth 2.0 / OIDC</h3>
<p>The dominant pattern for any non-trivial API in 2026. <a href="https://oauth.net/2/" target="_blank" rel="noopener noreferrer">OAuth 2.0</a> is the authorization framework - how a client gets a token to call your API on a user's behalf. <a href="https://openid.net/developers/how-connect-works/" target="_blank" rel="noopener noreferrer">OIDC</a> is the identity layer on top - how the client learns who the user actually is. Use OIDC for user authentication, OAuth 2.0 for delegated access. Implement <a href="https://oauth.net/2.1/" target="_blank" rel="noopener noreferrer">OAuth 2.1</a> conventions: PKCE everywhere, no implicit flow, no password grant, refresh-token rotation. The grant flows you'll actually use: authorization code + PKCE (web and mobile), client credentials (service-to-service), device code (TVs, CLIs).</p>
<h3>JWT (and the pitfalls)</h3>
<p>A JWT - JSON Web Token - is a signed (and optionally encrypted) blob of claims that the bearer can present to APIs. It's a transport, not an auth system. The pitfalls are notorious enough that JWT validation has its own published guide (<a href="https://datatracker.ietf.org/doc/html/rfc8725" target="_blank" rel="noopener noreferrer">RFC 8725</a> - JWT Best Current Practices). The recurring failure modes:</p>
<ul>
<li><strong><code>alg: none</code></strong> - the JWT header says "no signature, just trust me." Old libraries accepted this. Modern libraries reject it, but be explicit in your code: pin allowed algorithms.</li>
<li><strong>Algorithm confusion</strong> - token signed with HS256 (symmetric) is verified with RS256 (asymmetric) using the public key as the HMAC secret, succeeds. Pin the verification algorithm to the one your <a class="glossary-link" href="glossary.html#term-idp">IdP</a> issues.</li>
<li><strong><code>kid</code> path injection</strong> - the key-id header is a filesystem path or URL, an attacker controls it, your code reads from there. Treat kid as an opaque identifier you look up in a fixed table.</li>
<li><strong>Weak HMAC secrets</strong> - short, guessable HS256 secrets get cracked. Use asymmetric (RS256, ES256) keys; if you must use HMAC, use a high-entropy secret.</li>
<li><strong>No expiration check, no audience check, no issuer check</strong> - the most common bug. Every JWT validation must verify <code>exp</code>, <code>iat</code>, <code>iss</code>, <code>aud</code>, and signature.</li>
<li><strong>JWKS endpoint not pinned</strong> - your service trusts whichever JWKS the IdP serves. Pin the issuer URL; cache and rotate the JWKS deliberately; reject keys not in your trust set.</li>
<li><strong>Logout doesn't revoke</strong> - JWTs are stateless; "logout" doesn't invalidate the token. Use short expirations, refresh tokens with revocation, or a denylist for high-value sessions.</li>
</ul>
<h3>mTLS</h3>
<p>Mutual TLS - both ends present certificates. The strongest authentication on the network: a certificate can't be replayed off the wire by an HTTP intermediary, and the lifecycle is enforced by the CA. The operational cost is real (cert issuance, rotation, revocation, partner onboarding), so mTLS shows up most where the data is sensitive enough to justify it: service-to-service inside a <a href="service-mesh-security.html">service mesh</a>, high-value partner integrations, and any compliance regime that mandates client authentication.</p>
<h3>Session cookies (for browser-fronted APIs)</h3>
<p>If your API is consumed only by your own browser SPA on the same origin, a session cookie set after a login still works - Secure, HttpOnly, SameSite=Strict (or Lax, depending on cross-tab needs), and a server-side session store. The advantage is browser-supplied <a class="glossary-link" href="glossary.html#term-csrf">CSRF</a>/<a class="glossary-link" href="glossary.html#term-xss">XSS</a> defenses; the cost is no mobile or partner support. Most modern stacks use OAuth even for the SPA so the same backend supports browsers, mobile, and partners with one auth model.</p>
</section>
<section id="authz">
<h2>Authorization patterns</h2>
<p>Authentication says "this caller is X." Authorization says "X is allowed to do Y on resource Z." The dominant models:</p>
<h3>RBAC - Role-Based Access Control</h3>
<p>Permissions group into roles; users have roles. The simplest mental model, the easiest to audit, and what most internal admin tools and many B2B <a class="glossary-link" href="glossary.html#term-saas">SaaS</a> apps run on. Limitations show up when permission decisions depend on the resource itself (ownership, project, organization, classification) - at which point you bolt on <a class="glossary-link" href="glossary.html#term-abac">ABAC</a> or move up the stack.</p>
<h3>ABAC - Attribute-Based Access Control</h3>
<p>Decisions evaluated against attributes - user attributes, resource attributes, environment attributes, action attributes. More expressive than <a class="glossary-link" href="glossary.html#term-rbac">RBAC</a> for the "this user can read this object because they own it AND we're in business hours AND the object isn't classified" case. Harder to reason about; usually expressed in a policy language.</p>
<h3>The policy-as-code stack: OPA, Cedar, AuthZed/SpiceDB, Topaz</h3>
<ul>
<li><strong><a href="https://www.openpolicyagent.org/" target="_blank" rel="noopener noreferrer">Open Policy Agent (OPA)</a></strong> - <a class="glossary-link" href="glossary.html#term-cncf">CNCF</a> graduated, vendor-neutral, runs anywhere. <a class="glossary-link" href="glossary.html#term-rego">Rego</a> is the policy language. Used for API authorization, <a class="glossary-link" href="glossary.html#term-kubernetes">Kubernetes</a> admission, <a class="glossary-link" href="glossary.html#term-terraform">Terraform</a> pre-apply, and as a general <a class="glossary-link" href="glossary.html#term-pdp">policy decision point</a>. The most common "we want fine-grained authorization, don't want to invent a language" answer.</li>
<li><strong><a href="https://cedarpolicy.com/" target="_blank" rel="noopener noreferrer">Cedar</a></strong> - AWS-authored, open-source policy language. Pulls from the <a class="glossary-link" href="glossary.html#term-iam">IAM</a> heritage; strong analyzability story (can prove what a policy will and won't allow). Underpins <a href="https://aws.amazon.com/verified-permissions/" target="_blank" rel="noopener noreferrer">Amazon Verified Permissions</a>.</li>
<li><strong><a href="https://authzed.com/" target="_blank" rel="noopener noreferrer">AuthZed</a> / <a href="https://github.com/authzed/spicedb" target="_blank" rel="noopener noreferrer">SpiceDB</a></strong> - relationship-based authorization (<a class="glossary-link" href="glossary.html#term-rebac">ReBAC</a>), modeled after Google's Zanzibar paper. Best fit for the "who can read this document because they're in the team that owns the folder in the workspace" kind of permission graph.</li>
<li><strong><a href="https://www.topaz.sh/" target="_blank" rel="noopener noreferrer">Topaz</a></strong> - open-source authorization built on <a class="glossary-link" href="glossary.html#term-opa">OPA</a> and a directory model.</li>
<li><strong>Vendor: <a href="https://www.permit.io/" target="_blank" rel="noopener noreferrer">Permit.io</a>, <a href="https://www.styra.com/" target="_blank" rel="noopener noreferrer">Styra</a>, <a href="https://www.osohq.com/" target="_blank" rel="noopener noreferrer">Oso</a>, <a href="https://www.aserto.com/" target="_blank" rel="noopener noreferrer">Aserto</a></strong> - commercial overlays that bring management UI, audit logs, and policy lifecycle on top of these engines.</li>
</ul>
<h3>Scopes vs roles</h3>
<p>OAuth scopes are coarse permissions attached to a token at issuance ("this token can read calendars, not write them"). Roles are user-attached permissions resolved at request time. They serve different layers: scopes are what the user delegates to the client; roles are what the user is allowed to do, period. A common confusion: trying to express fine-grained per-object permissions as scopes (token explodes), or expressing client capability as roles (the user logically can do it, but you wanted to limit this app). The pragmatic split: scopes for client capability, roles/policies for user permissions, evaluated together at the API.</p>
</section>
<section id="bola">
<h2>The BOLA problem</h2>
<p>BOLA - Broken Object Level Authorization - is API1 on the OWASP top-10 for a reason. Almost every API has it somewhere, at least once in the codebase's history, because the bug fits naturally into how developers write controllers.</p>
<h3>Why it's nearly universal</h3>
<p>A typical handler:</p>
<pre><code>@app.get("/api/orders/{order_id}")
def get_order(order_id, user=current_user):
order = db.orders.find(order_id)
return order</code></pre>
<p>The framework's auth middleware confirmed <code>current_user</code> is logged in and can call this route. The handler looks up the order and returns it. The missing check: <em>does this user own this order?</em></p>
<p>The check is easy to write, but it has to be written into every handler that takes an object id - and one missed handler is a complete data-disclosure bug. Multiply by every list-style endpoint that paginates over objects the caller may not own, every nested resource (<code>/api/orgs/{org_id}/users/{user_id}/orders/{order_id}</code> - three checks), every PATCH that targets an object id in the body, every search that returns ids the caller can then fetch directly.</p>
<h3>How to design it out</h3>
<ul>
<li><strong>Scope queries through an authorization layer.</strong> Every database lookup goes through a method that takes <code>user_id</code> as well as <code>object_id</code>. <code>db.orders.find(order_id, owner_id=user.id)</code> returns the order only if the user owns it; the handler never has a path to bypass.</li>
<li><strong>Use opaque, unpredictable identifiers.</strong> UUIDs or hashids reduce the cost of a missed check - an attacker can't enumerate <code>/api/orders/1, /api/orders/2</code>. This is defense in depth, not the fix; the architectural fix is the ownership check.</li>
<li><strong>Centralize authorization in middleware.</strong> An <a href="#authz">OPA/Cedar/AuthZed-style</a> policy decision point reviews every request: caller, action, resource, attributes. Handlers don't write authz themselves; they call the PDP.</li>
<li><strong>Test the negative case.</strong> Integration tests that create two users, have user A request user B's objects, and assert 404 (not 403 - don't confirm existence) for every object endpoint. Generate the test from the route inventory.</li>
<li><strong>Detect at runtime.</strong> Cross-user object access in the logs is the signal. API security platforms (see <a href="#runtime">below</a>) build user-to-object models and alert when a user accesses objects outside their normal pattern.</li>
</ul>
<p>The mature answer is to make BOLA architecturally impossible - the database query itself enforces ownership - rather than relying on every developer to remember the check on every handler.</p>
</section>
<section id="rate-limit">
<h2>Rate limiting</h2>
<p>The crudest and most-effective control. Auth answers "who is calling?"; rate limiting answers "are they calling too much?" - and an attacker who can't make a million requests can't enumerate a million ids, brute-force a million passwords, or run a million expensive queries.</p>
<h3>What to limit on</h3>
<ul>
<li><strong>Per-IP</strong> - last resort, dodged by anyone with a botnet or residential-proxy network. Fine for anonymous public endpoints; not the primary defense.</li>
<li><strong>Per-user</strong> - the right granularity for authenticated APIs. Tied to the user-id or token, not the IP. Catches the "logged-in attacker enumerating objects" case.</li>
<li><strong>Per-API-key / per-client</strong> - for service-to-service and partner traffic.</li>
<li><strong>Per-endpoint</strong> - high-cost or sensitive endpoints (password reset, login, search, export) get tighter limits than read endpoints.</li>
<li><strong>Per-<a class="glossary-link" href="glossary.html#term-tenant">tenant</a></strong> - for B2B SaaS, the tenant is often the right limit unit, with sub-limits per user inside the tenant.</li>
</ul>
<h3>Algorithms</h3>
<p>Three you'll see in practice. <strong>Token bucket</strong> - refills at a rate, each request consumes a token; allows controlled bursts. <strong>Sliding window</strong> - count requests in the last N seconds. <strong>Fixed window with leak</strong> - count per discrete time bucket, with smoothing. Token bucket is the default for most gateways; sliding window is preferred where exact rate enforcement matters (anti-abuse, payment).</p>
<h3>Fair-use vs anti-abuse</h3>
<p>Two distinct concerns that get conflated. Fair-use limits prevent any one customer from monopolizing capacity - generous, mostly never hit, primarily defensive against bugs in client code. Anti-abuse limits target adversarial patterns - login brute-force, scraping, enumeration - and should be tight, surfaced as 429s, and tied to step-up auth or temporary blocking. Run both layers.</p>
<h3>Where it's enforced</h3>
<ul>
<li><strong>Edge / <a class="glossary-link" href="glossary.html#term-cdn">CDN</a></strong> - <a href="https://www.cloudflare.com/products/rate-limiting/" target="_blank" rel="noopener noreferrer">Cloudflare</a>, <a href="https://aws.amazon.com/waf/" target="_blank" rel="noopener noreferrer">AWS WAF rate-based rules</a>, <a href="https://learn.microsoft.com/en-us/azure/frontdoor/web-application-firewall" target="_blank" rel="noopener noreferrer">Azure Front Door</a>. Cheapest place to drop floods.</li>
<li><strong>Gateway</strong> - Kong, <a class="glossary-link" href="glossary.html#term-envoy">Envoy</a>, Apigee, AWS API Gateway, Azure APIM, Tyk. Per-route, per-consumer, per-user limits with policy expressions.</li>
<li><strong>App layer</strong> - for limits that need business context (per-org tier, per-feature flag). Redis-backed token buckets are the standard.</li>
</ul>
</section>
<section id="gateways">
<h2>API gateway landscape</h2>
<p>The API gateway is the <a class="glossary-link" href="glossary.html#term-pep">policy enforcement point</a> for north-south traffic - auth, rate limiting, schema validation, routing, observability. The pragmatic landscape:</p>
<ul>
<li><strong><a href="https://konghq.com/" target="_blank" rel="noopener noreferrer">Kong</a></strong> - open-source plus enterprise, plugin-rich, Lua-extensible, common in self-managed Kubernetes shops. Konnect is the SaaS control plane.</li>
<li><strong><a href="https://aws.amazon.com/api-gateway/" target="_blank" rel="noopener noreferrer">AWS API Gateway</a></strong> - REST API and HTTP API offerings. Native <a class="glossary-link" href="glossary.html#term-lambda">Lambda</a> integration, Cognito-friendly auth, WAF-friendly. The "you're already on AWS" default.</li>
<li><strong><a href="https://learn.microsoft.com/en-us/azure/api-management/" target="_blank" rel="noopener noreferrer">Azure API Management (APIM)</a></strong> - strong on developer portal, policy expressions in XML+C#, named values for secrets. Pairs with <a class="glossary-link" href="glossary.html#term-entra-id">Entra ID</a>, Defender for APIs.</li>
<li><strong><a href="https://cloud.google.com/apigee" target="_blank" rel="noopener noreferrer">GCP Apigee</a></strong> - enterprise-grade, came in via Google's 2016 acquisition; strong analytics and monetization. <a href="https://docs.cloud.google.com/api-gateway/docs/" target="_blank" rel="noopener noreferrer">GCP API Gateway</a> is the lightweight alternative for fronting Cloud Run / Cloud Functions.</li>
<li><strong><a href="https://www.cloudflare.com/products/api-shield/" target="_blank" rel="noopener noreferrer">Cloudflare API Gateway</a></strong> - gateway-plus-WAAP, runs at the Cloudflare edge, deep integration with the rest of the Cloudflare stack.</li>
<li><strong><a href="https://tyk.io/" target="_blank" rel="noopener noreferrer">Tyk</a></strong> - open-source plus enterprise; lightweight, Go-based, <a class="glossary-link" href="glossary.html#term-public">multi-cloud</a>.</li>
<li><strong><a href="https://www.krakend.io/" target="_blank" rel="noopener noreferrer">KrakenD</a></strong> - high-performance, stateless, configuration-as-code aggregation gateway.</li>
<li><strong><a href="https://www.solo.io/products/kgateway" target="_blank" rel="noopener noreferrer">Solo Gloo Gateway</a></strong> - Envoy-based, Kubernetes-native, gateway and mesh story under one vendor.</li>
<li><strong><a href="https://www.mulesoft.com/api/management" target="_blank" rel="noopener noreferrer">MuleSoft Anypoint</a></strong> - enterprise iPaaS + API management; common in large enterprise integration estates.</li>
<li><strong><a href="https://www.ibm.com/products/api-connect" target="_blank" rel="noopener noreferrer">IBM API Connect</a></strong> - enterprise; common in regulated industries with IBM-centric stacks.</li>
</ul>
<p>The choice usually breaks on three axes: how much you self-manage (Kong open-source vs Apigee managed vs Cloudflare edge), how cloud-native the integration is (AWS API Gateway is friction-free on AWS, friction elsewhere), and how strong the developer-portal and monetization story needs to be (Apigee and APIM lead here). For most cloud-native teams in 2026, the cloud-provider's gateway plus an API security platform layered on top is the right starting place.</p>
</section>
<section id="schema">
<h2>Schema validation</h2>
<p>The under-used control. If you declare what your API accepts - in <a href="https://swagger.io/specification/" target="_blank" rel="noopener noreferrer">OpenAPI 3.x</a>, <a href="https://json-schema.org/" target="_blank" rel="noopener noreferrer">JSON Schema</a>, or a protobuf definition - and enforce the declaration at the gateway, you eliminate an entire class of bugs: invalid types, oversized payloads, unknown fields (which is the mass-assignment defense), nested objects beyond a depth, arrays beyond a size.</p>
<h3>The principle</h3>
<p>"If it isn't in the schema, reject it" - the gateway returns 400 before the request ever reaches your handler. The handler can then trust that <code>order_id</code> is a UUID, <code>quantity</code> is a non-negative integer below the cap, and <code>customer</code> is an object with exactly the four allowed fields. Half of the input-validation bugs that show up in app code disappear at the gateway layer.</p>
<h3>The mechanics</h3>
<ul>
<li><strong>OpenAPI 3.1</strong> for REST - JSON Schema 2020-12 compatible, the practical standard. Generated from code (FastAPI, NestJS, Spring) or written first (spec-first design).</li>
<li><strong>JSON Schema</strong> directly for non-OpenAPI payload validation - webhook payloads, internal RPC schemas, queue messages.</li>
<li><strong>protobuf</strong> for gRPC - the type system is the schema.</li>
<li><strong>GraphQL SDL</strong> for GraphQL - the schema is mandatory and queries that don't match are rejected, but you still need to enforce field-level authorization beyond the schema's type checks.</li>
</ul>
<h3>Spec-first vs code-first</h3>
<p>Spec-first writes the OpenAPI doc first and generates code stubs from it; code-first writes the handlers and emits the OpenAPI doc from annotations. Spec-first produces cleaner, reviewable contracts and forces design before implementation. Code-first ships faster and stays in sync with the code automatically. The pragmatic answer: spec-first for partner-facing and v1 design; code-first with strict-mode validation for internal and iterative work. Either way: validate at the gateway, not just in tests.</p>
<h3>Tooling</h3>
<ul>
<li><a href="https://github.com/stoplightio/spectral" target="_blank" rel="noopener noreferrer">Spectral</a> - OpenAPI linter; catches missing auth, undocumented response codes, schema drift.</li>
<li><a href="https://42crunch.com/" target="_blank" rel="noopener noreferrer">42Crunch</a> - OpenAPI security audit and gateway-enforced contract validation.</li>
<li><a href="https://swagger.io/tools/swagger-codegen/" target="_blank" rel="noopener noreferrer">Swagger Codegen</a>, <a href="https://openapi-generator.tech/" target="_blank" rel="noopener noreferrer">OpenAPI Generator</a> - client and server stubs.</li>
<li>Gateway-native validation - AWS API Gateway request validators, Azure APIM <code>validate-content</code> policy, Kong OAS Validation plugin.</li>
</ul>
</section>
<section id="graphql">
<h2>GraphQL-specific risks</h2>
<p>GraphQL collapses many REST endpoints into one - and concentrates many REST-style bugs into different shapes.</p>
<h3>Introspection in production</h3>
<p>Introspection is the GraphQL feature that lets a client query the schema itself - every type, every field, every argument, every enum value, every deprecation. In development it's invaluable. In production, internet-facing, it's free recon for any attacker: they get a complete map of your API in seconds. Disable introspection in production or scope it to authenticated internal callers. Persisted queries (below) make introspection unnecessary for client builds anyway.</p>
<h3>Query depth and complexity attacks</h3>
<p>A GraphQL query can be arbitrarily deep - <code>{ user { friends { friends { friends { ... } } } } }</code> - and a small query string can fan out to enormous server work. Defenses: maximum query depth (typically 7-10), maximum query complexity score (each field weighted; query must score below a cap), and request timeouts. Use a library - <a href="https://www.npmjs.com/package/graphql-depth-limit" target="_blank" rel="noopener noreferrer">graphql-depth-limit</a>, <a href="https://github.com/slicknode/graphql-query-complexity" target="_blank" rel="noopener noreferrer">graphql-query-complexity</a>, or the framework's built-in equivalent.</p>
<h3>Batching attacks</h3>
<p>GraphQL supports batching - multiple queries in one request. An attacker can pack many enumeration queries (e.g., login attempts with different passwords) into a single request and bypass per-request rate limiting. Defenses: limit batch size, count each operation against rate limits separately, or disable batching.</p>
<h3>Persisted queries vs ad-hoc</h3>
<p>Persisted queries - the client registers a query template by hash with the server, and at runtime sends just the hash and variables - narrow the attack surface dramatically. Only registered queries run; ad-hoc queries are rejected. Apollo, Relay, and most clients support it. Mature GraphQL APIs in production use persisted queries; introspection becomes a build-time concern, not a runtime one.</p>
<h3>Field-level authorization</h3>
<p>The schema alone doesn't enforce authorization - a query for <code>user.salary</code> is valid GraphQL regardless of whether the caller can read salaries. Authorization has to live in the field resolvers (or in middleware that wraps them). This is where most GraphQL implementations bleed BOLA / BOPLA: the schema is correct, the per-field check is missing.</p>
<h3>Apollo Federation security</h3>
<p><a class="glossary-link" href="glossary.html#term-federation">Federation</a> composes multiple GraphQL subgraphs behind one gateway. The gateway and subgraphs must mutually authenticate (typically mTLS or signed JWTs), and subgraphs should not be directly reachable from clients. Misconfigured federation often leaves subgraphs exposed as full GraphQL endpoints with no auth - query them directly and skip the gateway's policy. Audit subgraph network <a class="glossary-link" href="glossary.html#term-reachability">reachability</a>.</p>
</section>
<section id="grpc">
<h2>gRPC-specific concerns</h2>
<p>gRPC is HTTP/2 + protobuf with strict typing - most of the input-validation problems REST and GraphQL face are eliminated by the wire format. Different concerns dominate:</p>
<ul>
<li><strong>Reflection in production.</strong> <a href="https://github.com/grpc/grpc/blob/master/doc/server-reflection.md" target="_blank" rel="noopener noreferrer">gRPC reflection</a> is GraphQL introspection's cousin - clients can ask the server for its full service definition at runtime. Useful for developer tooling, dangerous in production for the same reasons. Disable in prod.</li>
<li><strong>No built-in rate limiting.</strong> gRPC's HTTP/2 streams multiplex on a single connection; a per-connection rate limit doesn't translate. Use an L7 gateway (Envoy, Kong) for cross-cutting rate limiting, or a Redis-backed token bucket in interceptors.</li>
<li><strong>Binary protocol opacity for WAFs.</strong> Most WAFs do not parse protobuf; they see opaque binary frames. Mitigations: terminate gRPC at an Envoy-based gateway that can apply protocol-aware filters, deploy the API security platform's gRPC parser, or run the WAF in front of a REST/gRPC-gateway translation layer.</li>
<li><strong>Authentication.</strong> gRPC supports per-call credentials (token in metadata) and channel credentials (<a class="glossary-link" href="glossary.html#term-tls">TLS</a>, optionally mTLS). The mature pattern is mTLS for the channel plus a short-lived bearer token in metadata for the call.</li>
<li><strong>Authorization in interceptors.</strong> gRPC interceptors are the right place to apply central authorization - pulled from OPA, Cedar, or an internal PDP. Don't write authz into each handler.</li>
<li><strong>Long-lived streams.</strong> Bidirectional and server-streaming RPCs create long-lived connections. Define max-stream durations, idle timeouts, and abandoned-stream cleanup. Long streams also complicate rate limiting and abuse detection.</li>
</ul>
</section>
<figure class="page-photo">
<picture>
<source srcset="/img/photos/server-room.webp" type="image/webp">
<img src="/img/photos/server-room.jpg" alt="Rows of servers in a data center - internal endpoints that should never be reachable through SSRF" width="1880" height="1253" loading="lazy" decoding="async">
</picture>
<figcaption>The internal network behind your API - what an SSRF attacker is trying to reach.</figcaption>
</figure>
<section id="ssrf">
<h2>SSRF prevention in APIs</h2>
<p>SSRF - Server Side Request Forgery - is the bug where the server fetches a URL the attacker supplies. Common shapes: profile-picture URL fetch, webhook test endpoint, RSS importer, URL preview generator, OAuth callback URL, server-side image processing. The attacker's prize is access to the internal network - the metadata service, internal admin endpoints, the cloud's own APIs.</p>
<h3>The classic targets</h3>
<ul>
<li><strong>AWS instance metadata service (<a class="glossary-link" href="glossary.html#term-imds">IMDS</a>).</strong> <code>169.254.169.254</code> on <a class="glossary-link" href="glossary.html#term-ec2">EC2</a> returns temporary IAM credentials of the instance role. IMDSv2 (token-based, single-hop) is much harder to SSRF - <a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/" target="_blank" rel="noopener noreferrer">require it explicitly</a>. The 2019 Capital One breach was IMDSv1 SSRF.</li>
<li><strong>Azure Instance Metadata Service.</strong> Also at <code>169.254.169.254</code>, requires the <code>Metadata: true</code> header (a partial mitigation against trivial SSRF; not full).</li>
<li><strong>GCP metadata server.</strong> <code>metadata.google.internal</code> / <code>169.254.169.254</code>; <a href="https://docs.cloud.google.com/compute/docs/metadata/overview" target="_blank" rel="noopener noreferrer">requires the <code>Metadata-Flavor: Google</code> header</a>, which provides similar partial mitigation.</li>
<li><strong>Link-local and loopback.</strong> <code>127.0.0.0/8</code>, <code>::1</code>, <code>169.254.0.0/16</code> in IPv4; the IPv6 equivalents.</li>
<li><strong>Private RFC 1918 ranges.</strong> <code>10.0.0.0/8</code>, <code>172.16.0.0/12</code>, <code>192.168.0.0/16</code> - your internal subnets.</li>
<li><strong>Cloud-internal endpoints.</strong> Internal load balancers, internal-only APIs, the admin port of any service the attacker can name.</li>
</ul>
<h3>The defense pattern</h3>
<ul>
<li><strong>Allowlist by domain, not by deny-list.</strong> If you only need to fetch <code>*.partner.com</code>, allow exactly that. Deny-listing IP ranges is fragile; allow-listing is the default-deny correct answer.</li>
<li><strong>Resolve once, validate, then fetch.</strong> DNS rebinding attacks return a public IP to your allow-check and a private IP to the actual fetch. Resolve the hostname, validate the IP, then connect to the literal IP - not the hostname.</li>
<li><strong>Block link-local, loopback, and RFC1918 at the <a class="glossary-link" href="glossary.html#term-ingress">egress</a> layer.</strong> Defense in depth - the SSRF that bypasses your application code still hits an egress proxy that rejects internal addresses.</li>
<li><strong>Use an egress proxy.</strong> <a href="https://github.com/stripe/smokescreen" target="_blank" rel="noopener noreferrer">Stripe's smokescreen</a> and similar tools enforce centrally - every outbound call from the API service goes through a proxy that knows the allow-list, blocks metadata endpoints, and logs every fetch.</li>
<li><strong>Require IMDSv2 universally.</strong> Set <code>HttpTokens=required</code> on every EC2 instance; enforce via <a class="glossary-link" href="glossary.html#term-scp">SCP</a> or AWS Config rule. This single change neuters most cloud-credentials-via-SSRF attacks.</li>
<li><strong>Drop the response.</strong> If the application's only need is "does this URL exist?", make the HEAD request, don't return the body. Most SSRF that leaks credentials needs the response.</li>
</ul>
</section>
<section id="discovery">
<h2>API discovery & inventory</h2>
<p>The shadow-API problem is now a top-10 OWASP item (API9, Improper Inventory Management) because almost every org has it. Endpoints that exist in production but aren't in the inventory: old API versions you forgot to turn off, staging environments accessible from the internet, services your team didn't know about, endpoints behind a feature flag that's silently on.</p>
<h3>Three discovery sources</h3>
<ul>
<li><strong>From traffic.</strong> Mirror or sample gateway logs, parse for distinct paths, methods, and auth shape. The most accurate source - if it's getting traffic, it exists. Caveat: endpoints that exist but aren't being called still escape.</li>
<li><strong>From code.</strong> Scan repos for route definitions in known frameworks (Express, FastAPI, Spring, Rails, Gin). Catches endpoints not yet deployed and ones not seeing traffic. Caveat: dynamic route registration is hard to scan.</li>
<li><strong>From the cloud.</strong> Enumerate API Gateway / APIM / Apigee resources across every account and subscription. Catches the "someone span up an API Gateway in a side account" case CSPMs increasingly do this natively.</li>
</ul>
<h3>Tooling</h3>
<ul>
<li><strong>API security platforms</strong> - <a href="https://salt.security/" target="_blank" rel="noopener noreferrer">Salt Security</a>, <a href="https://www.traceable.ai/" target="_blank" rel="noopener noreferrer">Traceable</a>, <a href="https://www.akamai.com/products/api-security" target="_blank" rel="noopener noreferrer">Noname Security (Akamai)</a>, <a href="https://www.cequence.ai/" target="_blank" rel="noopener noreferrer">Cequence</a>, <a href="https://www.wallarm.com/" target="_blank" rel="noopener noreferrer">Wallarm</a>. All build inventory primarily from passive traffic analysis (gateway logs, mirrored traffic, or in-line agent).</li>
<li><strong>CSPM / <a class="glossary-link" href="glossary.html#term-cnapp">CNAPP</a></strong> - Wiz, Prisma Cloud, Orca, <a class="glossary-link" href="glossary.html#term-guardduty">Defender for Cloud</a> increasingly enumerate managed-API resources as part of the asset inventory.</li>
<li><strong>Code-first scanners</strong> - <a href="https://escape.tech/" target="_blank" rel="noopener noreferrer">Escape</a>, <a href="https://www.akto.io/" target="_blank" rel="noopener noreferrer">Akto</a>, and some of the API security platforms scan source repos in addition to traffic.</li>
</ul>
<p>Maturity goal: one inventory, deduplicated, that combines traffic, code, and cloud sources, with each entry mapped to an owning team and a documented data classification.</p>
</section>
<section id="runtime">
<h2>Runtime API security platforms</h2>
<p>The new analyst category. Vendors that sit on or alongside API traffic, build a behavioral model, and detect the business-logic attacks WAFs miss. The major players:</p>
<ul>
<li><strong><a href="https://salt.security/" target="_blank" rel="noopener noreferrer">Salt Security</a></strong> - early mover, cloud-delivered, strong analytics-led approach.</li>
<li><strong><a href="https://www.traceable.ai/" target="_blank" rel="noopener noreferrer">Traceable</a></strong> - distributed-tracing roots; deep east-west visibility plus north-south.</li>
<li><strong><a href="https://www.akamai.com/products/api-security" target="_blank" rel="noopener noreferrer">Noname Security</a></strong> - acquired by Akamai in 2024; now integrated with Akamai's WAAP.</li>
<li><strong><a href="https://www.cequence.ai/" target="_blank" rel="noopener noreferrer">Cequence</a></strong> - bot management heritage, strong on credential-stuffing and business-flow abuse.</li>
<li><strong><a href="https://www.akamai.com/" target="_blank" rel="noopener noreferrer">Akamai API Security</a></strong> - combination of Noname plus native Akamai capabilities.</li>
<li><strong><a href="https://www.wallarm.com/" target="_blank" rel="noopener noreferrer">Wallarm</a></strong> - open-source roots (Wallarm FAST), cloud and on-prem.</li>
<li><strong><a href="https://www.imperva.com/products/api-security/" target="_blank" rel="noopener noreferrer">Imperva</a></strong> - WAAP + API security from a long-time WAF vendor.</li>
<li><strong><a href="https://42crunch.com/" target="_blank" rel="noopener noreferrer">42Crunch</a></strong> - spec-first / OpenAPI-centric; complementary to traffic-based platforms.</li>
<li><strong><a href="https://www.pynt.io/" target="_blank" rel="noopener noreferrer">Pynt</a></strong> - focused on dev-time API security testing (DAST for APIs).</li>
</ul>
<h3>What they catch</h3>
<ul>
<li>Sequence-based abuse - login, password-reset, gift-card-balance enumeration patterns.</li>
<li>BOLA at runtime - user accessing object outside their normal pattern.</li>
<li>Shadow APIs - the inventory side.</li>
<li>Sensitive-data exposure - when an endpoint starts returning more data than it used to.</li>
<li>Drift from the OpenAPI spec - endpoints accepting fields that aren't documented.</li>
</ul>
<p>The platforms differ in deployment model (mirror / in-line / agent), where on the path they sit (CDN, gateway, <a class="glossary-link" href="glossary.html#term-sidecar">sidecar</a>, <a class="glossary-link" href="glossary.html#term-ebpf">eBPF</a>), and how heavily they lean on traffic analytics vs spec-based checks. Most large API estates end up with the WAAP / WAF for noise, plus one API security platform for the business-logic layer.</p>
</section>
<section id="testing">
<h2>API testing</h2>
<p>The testing toolchain mirrors AppSec: SAST-equivalent (scan the OpenAPI spec), DAST-equivalent (hit the running API with attack patterns), and fuzzing.</p>
<h3>DAST for APIs</h3>
<ul>
<li><a href="https://www.zaproxy.org/" target="_blank" rel="noopener noreferrer">OWASP ZAP</a> - open-source, API-aware via OpenAPI import.</li>
<li><a href="https://portswigger.net/burp" target="_blank" rel="noopener noreferrer">Burp Suite</a> - the de facto AppSec tool; strong API testing via the Burp REST scanner and extensions.</li>
<li><a href="https://www.pynt.io/" target="_blank" rel="noopener noreferrer">Pynt</a> - runs in CI from your Postman/OpenAPI collection.</li>
<li><a href="https://www.stackhawk.com/" target="_blank" rel="noopener noreferrer">StackHawk</a> - DAST built for CI/CD, API-first.</li>
<li><a href="https://detectify.com/" target="_blank" rel="noopener noreferrer">Detectify</a> - external attack-surface management with API testing.</li>
<li><a href="https://brightsec.com/" target="_blank" rel="noopener noreferrer">Bright Security</a> - <a class="glossary-link" href="glossary.html#term-ai">AI</a>-augmented DAST with API focus.</li>
</ul>
<h3>Fuzzing</h3>
<ul>
<li>Postman + Newman - collection-driven, schema-aware fuzzing for REST APIs.</li>
<li><a href="https://github.com/redhat-cip/restfuzz" target="_blank" rel="noopener noreferrer">RestFuzz</a> - OpenAPI-driven REST fuzzer.</li>
<li><a href="https://github.com/microsoft/restler-fuzzer" target="_blank" rel="noopener noreferrer">RESTler (Microsoft)</a> - stateful REST API fuzzer that infers operation dependencies from the spec.</li>
<li><a href="https://schemathesis.readthedocs.io/en/stable/" target="_blank" rel="noopener noreferrer">Schemathesis</a> - property-based testing from OpenAPI and GraphQL schemas.</li>
</ul>
<h3>Contract testing</h3>
<ul>
<li><a href="https://pact.io/" target="_blank" rel="noopener noreferrer">Pact</a> - consumer-driven contracts; pre-commit verification that consumer expectations match provider behavior.</li>
<li><a href="https://dredd.org/en/latest/" target="_blank" rel="noopener noreferrer">Dredd</a> - OpenAPI-validated end-to-end test runner.</li>
</ul>
<p>Run schema linting (Spectral) in PR; DAST + fuzzing in nightly CI; manual Burp / ZAP for major releases; <a href="cloud-pentesting.html">third-party pentests</a> for critical APIs annually.</p>
</section>
<section id="webhooks">
<h2>Webhooks security</h2>
<p>Webhooks are the inverse of an API call - your system receives unsolicited HTTP POSTs from a third party (Stripe, GitHub, SaaS integrations). The defenses inherit some, invert others:</p>
<ul>
<li><strong>Signature verification.</strong> Provider signs the payload with a shared secret or asymmetric key; you verify. Stripe's <a href="https://docs.stripe.com/webhooks#verify-events" target="_blank" rel="noopener noreferrer">webhook signature pattern</a> is the industry reference. Verify before parsing - a forged unsigned payload should never reach your handler.</li>
<li><strong>Replay protection.</strong> Signature alone isn't enough - an attacker who captures a valid webhook can replay it. Include a timestamp in the signed payload, reject requests older than a few minutes, and track recently-seen idempotency keys to drop duplicates.</li>
<li><strong>Idempotency on retries.</strong> Webhook providers retry on non-2xx responses. Your handler must be idempotent - applying the same webhook twice produces the same outcome. Track <code>idempotency_key</code> or use a deduplication store.</li>
<li><strong>Allowlist by source IP and signature.</strong> Many webhook providers publish their egress IP ranges. Use them as a coarse filter; signature is the fine filter.</li>
<li><strong>Authenticate yourself to the webhook target.</strong> If you are the provider, mTLS your webhook deliveries or sign them per the above pattern; never accept "we can't verify the source" as a configuration.</li>
<li><strong>Limit the response.</strong> Don't echo request data in the response; don't return verbose errors. The other side doesn't need them, and they're a SSRF leak.</li>
</ul>
</section>
<section id="third-party">
<h2>Third-party API consumption (you're being attacked through APIs you call)</h2>
<p>API10 in the 2023 list - Unsafe Consumption of APIs - formalizes a class of risk most teams under-weight. Your code calls a third-party API and trusts the response. The upstream provider gets compromised, returns malicious data, and you ingest it.</p>
<ul>
<li><strong>Treat third-party responses as untrusted input.</strong> Same input-validation rules apply: validate against a schema you control, reject unexpected fields, enforce length and type limits.</li>
<li><strong>Watch for unsafe deserialization.</strong> The most common pattern is <code>pickle.loads(response.content)</code> or equivalent - never deserialize unsigned data from anywhere.</li>
<li><strong>Set timeouts on every outbound call.</strong> No default timeout in your HTTP client is a slow-loris attack waiting to happen if the upstream is slow or malicious.</li>
<li><strong>Use circuit breakers.</strong> Upstream failures shouldn't cascade - <a href="https://github.com/Netflix/Hystrix" target="_blank" rel="noopener noreferrer">Hystrix</a>-style libraries (now <a href="https://resilience4j.readme.io/" target="_blank" rel="noopener noreferrer">Resilience4j</a>, <a href="https://github.com/sony/gobreaker" target="_blank" rel="noopener noreferrer">gobreaker</a>, etc.) trip after N consecutive failures and fail fast.</li>
<li><strong>Manage secrets carefully.</strong> Third-party API keys are bearer credentials. Store in a <a class="glossary-link" href="glossary.html#term-secrets-manager">secrets manager</a> (AWS Secrets Manager, Azure <a class="glossary-link" href="glossary.html#term-key-vault">Key Vault</a>, GCP Secret Manager, HashiCorp Vault); rotate; never log; never embed in code.</li>
<li><strong>Retry with exponential backoff.</strong> Tight retry loops on a transient upstream failure compound the incident. Library-supported backoff with jitter is the default.</li>
<li><strong>Vendor risk management.</strong> The <a href="grc.html">GRC program</a> tracks third-party API providers as subprocessors when they handle customer data; the security review is on the same cadence as any other vendor.</li>
</ul>
</section>
<section id="mesh">
<h2>APIs in the service mesh (east-west)</h2>
<p>This page is the north-south view - clients on the outside, your APIs on the inside, the gateway in the middle. The east-west view - service-to-service traffic inside the cluster - is the <a href="service-mesh-security.html">service mesh page</a>. The two are complementary:</p>
<ul>
<li><strong>North-south.</strong> Untrusted clients, OAuth-issued tokens, gateway enforcement, request-level rate limiting. Higher payload variability, lower trust.</li>
<li><strong>East-west.</strong> Mutually-authenticated services, mTLS by default, sidecar-enforced policy, much higher trust baseline but the same blast-radius rule: a compromised service shouldn't be able to call the whole cluster.</li>
</ul>
<p>Where they overlap: a request that arrived at the gateway with a user identity needs to propagate that identity through the mesh so downstream services authorize against the user, not against the calling service. Token propagation, <a href="https://datatracker.ietf.org/doc/html/rfc9068" target="_blank" rel="noopener noreferrer">RFC 9068 JWT profile for OAuth</a>, and <a class="glossary-link" href="glossary.html#term-spiffe">SPIFFE</a>-style <a class="glossary-link" href="glossary.html#term-service-account">workload identity</a> are the connective tissue.</p>
</section>
<section id="ai-apis"><h2>Securing AI, LLM, and agent APIs</h2>
<p>An <a class="glossary-link" href="glossary.html#term-llm">LLM</a> or <a class="glossary-link" href="glossary.html#term-agent">agent</a> endpoint is still an API. Everything on this page still applies: authentication, authorization, rate limiting, and schema validation on every request. What changes is that these APIs take natural language and tool calls as input and produce actions as output, so they add threat classes that a classic API security program never had to model. Treat the model as one more untrusted component in the request path, not as a trusted decision-maker.</p>
<h3>The OWASP Top 10 for LLM Applications is the API-layer companion to the classic list</h3>
<p>The <a href="https://owasp.org/www-project-api-security/" target="_blank" rel="noopener noreferrer">OWASP API Security Top 10</a> still governs the transport and auth layer. Map the model-specific risks against the <a href="https://genai.owasp.org/llm-top-10/" target="_blank" rel="noopener noreferrer">OWASP Top 10 for LLM Applications (2025)</a> and govern the program under the <a href="https://www.nist.gov/itl/ai-risk-management-framework" target="_blank" rel="noopener noreferrer">NIST AI Risk Management Framework</a>. Run both lists together: an LLM feature that fixes prompt injection but ships broken object-level authz is still trivially exploitable at the API layer. See <a href="ai-ml-security.html">AI/ML security</a> for the model-lifecycle view, and the <a href="ai-learning.html">AI learning path</a> if you are ramping up on the space.</p>
<h3>Prompt injection is an input-trust problem</h3>
<p><a class="glossary-link" href="glossary.html#term-prompt-injection">Prompt injection</a> is the API-layer failure to distinguish trusted instructions from untrusted content. Any text the model reads can carry instructions: user input, tool and function outputs, and <a class="glossary-link" href="glossary.html#term-rag">RAG</a> documents pulled from wikis, tickets, or the open web. <a href="https://genai.owasp.org/llmrisk/llm01-prompt-injection/" target="_blank" rel="noopener noreferrer">OWASP LLM01:2025</a> is explicit that indirect injection through retrieved content is the harder case. Treat every byte reaching the model as untrusted, and never let raw model output directly authorize a privileged API call. The <a href="breaches/promptware.html">Promptware kill chain</a> shows this end to end: a poisoned document steers the model into calling real tools with attacker intent.</p>
<h3>Tool and function-calling authorization: same root cause, new trigger</h3>
<p>When an LLM calls your functions, that is a request to your API and it must be authorized like any other. This is <a class="glossary-link" href="glossary.html#term-bola">broken object level authorization</a> with a new trigger: instead of a scripted client tampering with an ID, the model is coaxed into requesting the wrong object. Enforce the <em>caller's</em> identity and permissions server-side on every tool invocation - do not delegate the check to the model's judgment or to a system prompt. Pass the end user's scoped credential, not a shared service token, and apply <a class="glossary-link" href="glossary.html#term-least-privilege">least privilege</a> and <a href="zero-trust.html">zero trust</a> per call. Ground tool identities in your <a href="non-human-identity.html">non-human identity</a> and <a href="iam.html">IAM</a> program.</p>
<h3>Budgets, output validation, and the agent tool surface</h3>
<ul>
<li><strong>Per-user and per-key budgets.</strong> Meter token and cost budgets and rate limits by identity, not just globally, to bound denial-of-wallet abuse where a single caller drives unbounded inference spend.</li>
<li><strong>Validate output before it moves downstream.</strong> Schema-check and content-filter model output before it reaches a shell, database, browser, or another API. Model output is untrusted input to the next system.</li>
<li><strong>Treat agent tool APIs as new attack surface.</strong> The <a class="glossary-link" href="glossary.html#term-mcp">MCP</a> and agent tool layer is a fresh, under-hardened surface - see <a href="mcp-security.html">MCP security</a>. Instrument tool calls and injection attempts through <a href="detection-engineering.html">detection engineering</a> so abuse is observable, not silent.</li>
</ul>
</section>
<section id="comparison">
<h2>AWS, Azure, and GCP side-by-side</h2>
<p>The native API security capabilities each cloud ships, reduced to a one-screen reference:</p>
<div class="comparison-table">
<table>
<thead>
<tr>
<th scope="col" class="ct-row-head">Capability</th>
<th scope="col" class="ct-aws"><span class="ct-dot ct-dot-aws" aria-hidden="true"></span> AWS</th>
<th scope="col" class="ct-azure"><span class="ct-dot ct-dot-azure" aria-hidden="true"></span> Azure</th>
<th scope="col" class="ct-gcp"><span class="ct-dot ct-dot-gcp" aria-hidden="true"></span> GCP</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">API gateway</th>
<td>API Gateway (REST + HTTP), AppSync (GraphQL)</td>
<td>API Management (APIM)</td>
<td>Apigee, API Gateway</td>
</tr>
<tr>
<th scope="row">Auth integration</th>
<td>Cognito, IAM, Lambda authorizers, JWT authorizers</td>
<td>Entra ID, OAuth/OIDC, certificate auth, JWT validation</td>
<td>Identity Platform, <a class="glossary-link" href="glossary.html#term-iap">IAP</a>, custom token verification</td>
</tr>
<tr>
<th scope="row">Fine-grained authz</th>
<td>Verified Permissions (Cedar)</td>
<td>Custom + Entra ID / RBAC</td>
<td>IAM Conditions, custom</td>
</tr>
<tr>
<th scope="row">Rate limiting</th>
<td>API Gateway usage plans, WAF rate-based rules</td>
<td>APIM rate-limit policy, Front Door rate limiting</td>
<td>Apigee quota policy, <a class="glossary-link" href="glossary.html#term-cloud-armor">Cloud Armor</a> rate-based rules</td>
</tr>
<tr>
<th scope="row">Schema validation</th>
<td>Request validators (OpenAPI in import)</td>
<td>APIM validate-content policy</td>
<td>Apigee OASValidation policy</td>
</tr>
<tr>
<th scope="row">API security platform</th>
<td>None native (3rd party); WAF + Inspector</td>
<td>Microsoft Defender for APIs (in APIM)</td>
<td>Apigee Advanced API Security</td>
</tr>
<tr>
<th scope="row">WAF for APIs</th>
<td>AWS WAF + Bot Control + Fraud Control</td>
<td>Azure WAF on Front Door / App Gateway</td>
<td>Cloud Armor with reCAPTCHA Enterprise</td>
</tr>
<tr>
<th scope="row">Discovery</th>
<td>Across accounts via Config + custom; 3rd party CSPM/CNAPP</td>
<td>Defender for APIs auto-discovery within APIM</td>
<td>Apigee API Hub, SCC asset inventory</td>
</tr>
<tr>
<th scope="row">mTLS support</th>
<td>API Gateway custom domain w/ truststore (<a class="glossary-link" href="glossary.html#term-s3">S3</a>-hosted)</td>
<td>APIM client certificate auth</td>
<td>Apigee mTLS, Cloud Load Balancing mTLS</td>
</tr>
<tr>
<th scope="row">Observability</th>
<td>CloudWatch, X-Ray, API Gateway access logs</td>
<td>App Insights, APIM diagnostics, Log Analytics</td>
<td>Cloud Logging, Cloud Trace, Apigee analytics</td>
</tr>
</tbody>
</table>
</div>
<p>The native tools cover the basics well; the runtime business-logic detection is still where third-party API security platforms add the most value, especially for organizations whose APIs are the product (any B2B SaaS, fintech, healthtech, marketplace).</p>
</section>
<section id="maturity">
<h2>Maturity stages</h2>
<p>A staging model that maps to the work most programs actually do:</p>
<div class="pillar-grid">
<div class="pillar-card pillar-card-stage1">
<h3>Stage 1 - Tribal-knowledge</h3>
<p>APIs exist; no shared inventory. Auth is whatever each team picked. Some have rate limits; others don't. OWASP API Top 10 is something the AppSec team has read; engineering hasn't. The first breach or close call kicks off the next stage.</p>
</div>
<div class="pillar-card pillar-card-stage2">
<h3>Stage 2 - Documented</h3>
<p>OpenAPI specs exist for the major APIs. The auth pattern is standardized (typically OAuth + JWT through an identity provider). Rate limits are deployed on the obvious endpoints (login, password reset, search). An owner is listed for each major API.</p>
</div>
<div class="pillar-card pillar-card-stage3">
<h3>Stage 3 - Gated</h3>
<p>An API gateway fronts every external API; schema validation is enforced; rate limits are per-user. Authentication is centralized. BOLA tests run in CI for every endpoint. Webhook signature verification is mandatory.</p>
</div>
<div class="pillar-card pillar-card-stage4">
<h3>Stage 4 - Observed</h3>
<p>An API security platform watches production traffic, builds the inventory, alerts on anomalies. Shadow APIs surface within days of going live. Drift from OpenAPI specs is detected. Cross-user object access is flagged.</p>
</div>
<div class="pillar-card pillar-card-stage5">
<h3>Stage 5 - Defended</h3>
<p>Authorization is architecturally enforced through a central PDP (OPA / Cedar / AuthZed); BOLA is structurally impossible. Persisted queries for GraphQL; reflection disabled for gRPC; introspection scoped. The API security platform's behavioral models drive auto-block decisions on the worst patterns. The program is part of the product roadmap, not a separate audit.</p>
</div>
</div>
</section>
<section id="pitfalls">
<h2>Common pitfalls</h2>
<ul>
<li><strong>Assuming the WAF covers APIs.</strong> WAFs catch L7 attack signatures; they don't catch business-logic abuse. The biggest API bugs (BOLA, BFLA, BOPLA, unrestricted resource consumption) are invisible to WAF rule sets.</li>
<li><strong>JWT trust without validation.</strong> Decoding the JWT and reading the claims without verifying the signature, audience, issuer, and expiration. A surprisingly common bug across libraries, especially in handlers that "just need the user id."</li>
<li><strong>No rate limit on auth endpoints.</strong> Login, password reset, <a class="glossary-link" href="glossary.html#term-mfa">MFA</a> verification, token refresh - all need tighter rate limits than the rest of the API. Brute force is most painful where the rate-limiter is loosest.</li>
<li><strong>No schema validation.</strong> Endpoints accept any JSON, parse it permissively, and trust the result. Schema validation at the gateway eliminates an entire class of bugs and is a same-day implementation if you already have an OpenAPI spec.</li>
<li><strong>GraphQL introspection / gRPC reflection in production.</strong> Free recon for any attacker. Disable in prod; the dev experience can be preserved with persisted queries or a non-prod environment.</li>
<li><strong>Secrets in URLs.</strong> API keys, tokens, session ids in query strings - they end up in access logs, browser history, referer headers, and CDN caches. Put credentials in headers; rotate any that leaked into a URL.</li>
<li><strong>No inventory.</strong> Without a continuously-maintained inventory, you can't answer "is this endpoint still in use," "who owns it," "what data does it touch?" The shadow-API problem grows silently.</li>
<li><strong>BOLA in every codebase that hand-rolls authz.</strong> Every endpoint that takes an object id has to check ownership; the architectural fix is making the check happen automatically (scoped queries, central PDP), not relying on developer discipline.</li>
<li><strong>Webhook handlers that trust the body before verifying the signature.</strong> Parse-then-verify is wrong; verify-then-parse is correct. Same logic for replay timestamps.</li>
<li><strong>SSRF via "fetch a URL the user supplied."</strong> Every URL-import, image-from-link, webhook-test, RSS-importer feature is an SSRF candidate. Egress-proxy, allow-list, and IMDSv2 are the layered defense.</li>
</ul>
</section>