File size: 46,418 Bytes
4058c6c
66083cf
 
 
 
fa3f21f
 
66083cf
 
 
 
 
 
 
 
 
 
 
 
 
 
fa3f21f
66083cf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fa3f21f
66083cf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fa3f21f
 
a9267da
fa3f21f
 
66083cf
 
 
 
 
fa3f21f
 
 
 
 
 
 
 
 
 
 
 
4058c6c
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
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>OpenAPI authentication check — check your api specification free</title>
<meta name="description" content="Find OpenAPI endpoints published without authentication, API keys in query strings, and OAuth grants forbidden by RFC 9700.">
<style>
:root{color-scheme:light}body{margin:0;font:15px/1.55 system-ui,-apple-system,"Segoe UI",sans-serif;background:#faf9f5;color:#1a1512}
.wrap{max-width:960px;margin:0 auto;padding:28px 18px 60px}h1{font-size:26px;margin:0 0 6px}.lede{color:#4a403a;margin:0 0 16px}
textarea{width:100%;min-height:260px;font:13px/1.45 ui-monospace,Menlo,Consolas,monospace;padding:12px;border:1px solid #d9d0c4;border-radius:8px;box-sizing:border-box}
.bar{display:flex;gap:10px;align-items:center;margin:12px 0}button{font:600 15px system-ui;padding:10px 18px;border:0;border-radius:8px;background:#1a1512;color:#fff;cursor:pointer}
input[type=date]{font:14px system-ui;padding:8px;border:1px solid #d9d0c4;border-radius:6px}
table{border-collapse:collapse;width:100%;font-size:14px;margin-top:10px}th,td{text-align:left;padding:8px 10px;border-bottom:1px solid #eee;vertical-align:top}th{font-size:12px;letter-spacing:.06em;text-transform:uppercase;color:#7a6e65}
.sev.error{color:#a8551f;font-weight:700}.sev.warn,.sev.warning{color:#96691f}.ok{color:#2a6446;font-weight:600}.sum{font-weight:600}
.note{font-size:13px;color:#7a6e65;margin-top:18px}#paid{margin-top:22px;padding:14px 16px;border:1px solid #e2d8cb;border-radius:10px;background:#fffdfa}
</style>
<script data-grow-initializer="">!(function(){window.growMe||((window.growMe=function(e){window.growMe._.push(e);}),(window.growMe._=[]));var e=document.createElement("script");(e.type="text/javascript"),(e.src="https://faves.grow.me/main.js"),(e.defer=!0),e.setAttribute("data-grow-faves-site-id","U2l0ZTphZjljMzgyMC0yMmU5LTRhYjgtYTIxNS03OTRhMGY1N2ExYzM=");var t=document.getElementsByTagName("script")[0];t.parentNode.insertBefore(e,t);})();</script>
<!--yj-canon-s101--><link rel="canonical" href="https://getreadystack.com/tools/openapi-security-contract-lint"></head>
<body>
<div class="wrap">
<h1>OpenAPI authentication check — check your api specification free</h1>
<p class="lede">Finds the lines in an OpenAPI file that publish an endpoint with no authentication, an API key in the query string, or an OAuth grant that RFC 9700 forbids. Runs entirely in your browser — nothing is uploaded.</p>
<textarea id="src" spellcheck="false">openapi: 3.0.3
info:
  title: Billing API
  version: 2026.09.1
servers:
  - url: http://api.example.com/v2
paths:
  /invoices:
    get:
      summary: List invoices
      security: []
      responses:
        '200':
          description: A page of invoices
    post:
      summary: Create an invoice
      deprecated: true
      security:
        - apiKeyAuth: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
              properties:
                amountMinor:
                  type: integer
                callbackToken:
                  type: string
                  example: sk_live_4eC39HqLyjWDarjtT1zdp7dc
      responses:
        '201':
          description: Invoice created
  /debug/vars:
    get:
      summary: Runtime counters
      responses:
        '200':
          description: Counters
components:
  securitySchemes:
    apiKeyAuth:
      type: apiKey
      in: query
      name: api_key
    basicAuth:
      type: http
      scheme: basic
    oauth2Auth:
      type: oauth2
      flows:
        implicit:
          authorizationUrl: http://auth.example.net/authorize
          scopes: {}
        password:
          tokenUrl: http://auth.example.net/token
          scopes:
            '*': Everything this API can do</textarea>
<div class="bar"><button id="go">Check</button><label>as of <input type="date" id="today"></label></div>
<div id="out" aria-live="polite"></div>
<div id="paid"><strong>Free here:</strong> Audit the OpenAPI file open in the editor, line by line. <strong>In the editor:</strong> Sweep every OpenAPI file in the workspace and keep a dated report (CSV/JSON/HTML) as release evidence — that part asks for a licence key.<br><a href="https://getreadystack.com/api/buy/cl/polar_cl_nXjacvEwIBQ8xjkXZXDymfZbCc9cnushVFkc83YVTJW">Sweep the whole workspace and keep the dated report &mdash; $29 once</a><br><span class="note">$29 once &middot; one licence key per person or CI seat &middot; An application-security consultant reading the same contract by hand bills $150-$250 an hour.</span></div>
<p class="note">Same engine as the VS Code extension, byte for byte.</p>
</div>
<script>var OAS_RULES = [
  {"id":"security_empty_operation","sev":"error","msg":"security: [] on an operation means \"no authentication required\". A gateway that imports this spec publishes the endpoint open.","ref":"OWASP API Security Top 10 2023 - API1 / API5"},
  {"id":"security_empty_global","sev":"error","msg":"Top-level security: [] declares the whole API public. Every operation that does not override it is unauthenticated.","ref":"OWASP API Security Top 10 2023 - API5"},
  {"id":"no_global_security","sev":"warning","msg":"This document has paths but no top-level security block. Authentication is then opt-in per operation, so any operation you forget ships open.","ref":"OpenAPI 3.1 section 4.8.1 - Security Requirement Object"},
  {"id":"apikey_in_query","sev":"error","msg":"An apiKey carried in the query string is written to proxy logs, browser history and Referer headers. Move it to a header.","ref":"OWASP API Security Top 10 2023 - API2"},
  {"id":"oauth_implicit_flow","sev":"error","msg":"The OAuth 2.0 implicit grant returns the token in the URL fragment. RFC 9700 (BCP 240, January 2025) says clients MUST NOT use it.","ref":"RFC 9700 section 2.1.2"},
  {"id":"oauth_password_flow","sev":"error","msg":"The resource owner password credentials grant hands the user's password to the client. RFC 9700 (BCP 240, January 2025) says it MUST NOT be used.","ref":"RFC 9700 section 2.4"},
  {"id":"oauth_url_plaintext","sev":"error","msg":"An OAuth endpoint reached over http:// exposes the authorization code or token to anyone on the path.","ref":"RFC 9700 section 2.6"},
  {"id":"server_plaintext_http","sev":"error","msg":"A server entry on http:// tells every generated client and gateway to send credentials in the clear.","ref":"OWASP API Security Top 10 2023 - API8"},
  {"id":"no_https_server","sev":"warning","msg":"No server in this document uses https://. There is no transport-secured base URL for a generated client to pick.","ref":"OWASP API Security Top 10 2023 - API8"},
  {"id":"basic_auth_scheme","sev":"warning","msg":"HTTP Basic replays a static credential on every call, so it cannot be revoked per session and shows up in memory dumps and logs.","ref":"OWASP API Security Top 10 2023 - API2"},
  {"id":"oauth_scopes_empty","sev":"warning","msg":"An OAuth flow with no scopes issues one all-or-nothing token, so a read client holds write rights.","ref":"OWASP API Security Top 10 2023 - API5"},
  {"id":"scope_wildcard","sev":"warning","msg":"A wildcard or catch-all scope defeats the point of scopes: every token that asks for it gets the whole API.","ref":"OWASP API Security Top 10 2023 - API5"},
  {"id":"scheme_defined_unused","sev":"warning","msg":"This security scheme is defined but never referenced by any security block, so nothing in the document actually requires it.","ref":"OpenAPI 3.1 section 4.8.1"},
  {"id":"missing_429","sev":"warning","msg":"This operation documents no 429 response, so generated clients and the gateway have no agreed throttling contract.","ref":"OWASP API Security Top 10 2023 - API4"},
  {"id":"additional_properties_open","sev":"warning","msg":"additionalProperties: true on a request schema lets a caller post fields you never declared - the classic mass-assignment route.","ref":"OWASP API Security Top 10 2023 - API6"},
  {"id":"deprecated_without_sunset","sev":"warning","msg":"An operation is marked deprecated but the document names no sunset date, so callers have no removal date to plan against.","ref":"RFC 8594 - The Sunset HTTP Header Field"},
  {"id":"internal_path_published","sev":"warning","msg":"A debug, trace or actuator path is published in the same document you hand to external callers.","ref":"OWASP API Security Top 10 2023 - API9"},
  {"id":"secret_in_example","sev":"error","msg":"This example or default value carries a live-looking credential. Examples are copied into docs, SDKs and test fixtures.","ref":"OWASP API Security Top 10 2023 - API8"}
]
;</script>
<script>'use strict';
// OpenAPI Security Contract Lint - one brain, used by the extension and by the free web page.
var RULES = (typeof module !== 'undefined' && module.exports) ? require('./rules.json') : window.OAS_RULES;

var BY_ID = {};
for (var i = 0; i < RULES.length; i++) BY_ID[RULES[i].id] = RULES[i];

function indentOf(line) { var m = line.match(/^(\s*)/); return m[1].replace(/\t/g, '  ').length; }
function esc(s) { return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }

// Collect the body lines of a block whose header sits at `line` (0-based) with indent `ind`.
function blockRange(lines, start, ind) {
  var end = start + 1;
  while (end < lines.length) {
    var l = lines[end];
    if (l.trim() !== '' && !/^\s*#/.test(l) && indentOf(l) <= ind) break;
    end++;
  }
  return end;
}

function check(text, opts) {
  opts = opts || {};
  var lines = String(text == null ? '' : text).split(/\r?\n/);
  var findings = [];
  function add(id, line, extra) {
    var r = BY_ID[id];
    if (!r) return;
    findings.push({ check: id, sev: r.sev, msg: (extra ? extra + ' ' : '') + r.msg, line: line });
  }

  var hasPaths = false, pathsIndent = -1, pathsLine = 0;
  var hasGlobalSecurity = false;
  var serversSeen = false, httpsServerSeen = false;
  var schemesStart = -1, schemesEnd = -1, schemesIndent = -1;
  var deprecatedLines = [];
  var sunsetSeen = /(^|[\s"'_-])sunset/i.test(text);

  // ---- pass 1: structure ----
  for (var n = 0; n < lines.length; n++) {
    var raw = lines[n], t = raw.trim(), ind = indentOf(raw);
    if (t === '' || t.charAt(0) === '#') continue;
    if (ind === 0 && /^"?paths"?\s*:/.test(t)) { hasPaths = true; pathsIndent = ind; pathsLine = n + 1; }
    if (ind === 0 && /^"?security"?\s*:/.test(t)) {
      hasGlobalSecurity = true;
      if (/^"?security"?\s*:\s*\[\s*\]\s*,?$/.test(t)) add('security_empty_global', n + 1);
    }
    if (ind === 0 && /^"?servers"?\s*:/.test(t)) serversSeen = true;
    if (/^"?securitySchemes"?\s*:/.test(t) && schemesStart < 0) {
      schemesStart = n; schemesIndent = ind; schemesEnd = blockRange(lines, n, ind);
    }
  }

  // ---- pass 2: line rules ----
  var flowsIndent = -1, flowName = null, flowNameIndent = -1;
  for (var n2 = 0; n2 < lines.length; n2++) {
    var line = lines[n2], s = line.trim(), ind2 = indentOf(line), ln = n2 + 1;
    if (s === '' || s.charAt(0) === '#') continue;

    if (/^-?\s*"?url"?\s*:\s*["']?https:\/\//i.test(s)) httpsServerSeen = true;
    if (/^-?\s*"?url"?\s*:\s*["']?http:\/\//i.test(s) && !/http:\/\/(localhost|127\.0\.0\.1|\[::1\])/i.test(s)) {
      add('server_plaintext_http', ln);
    }
    if (/^"?(authorizationUrl|tokenUrl|refreshUrl)"?\s*:\s*["']?http:\/\//i.test(s) && !/http:\/\/(localhost|127\.0\.0\.1)/i.test(s)) {
      add('oauth_url_plaintext', ln);
    }
    if (ind2 > 0 && /^"?security"?\s*:\s*\[\s*\]\s*,?$/.test(s)) add('security_empty_operation', ln);
    if (/^"?in"?\s*:\s*["']?query["']?\s*,?$/.test(s) && schemesStart >= 0 && n2 > schemesStart && n2 < schemesEnd) {
      add('apikey_in_query', ln);
    }
    if (/^"?scheme"?\s*:\s*["']?basic["']?\s*,?$/i.test(s) && schemesStart >= 0 && n2 > schemesStart && n2 < schemesEnd) {
      add('basic_auth_scheme', ln);
    }
    if (/^"?flows"?\s*:/.test(s)) { flowsIndent = ind2; continue; }
    if (flowsIndent >= 0) {
      if (ind2 <= flowsIndent) { flowsIndent = -1; flowName = null; flowNameIndent = -1; }
      else if (ind2 === flowsIndent + 2 || (flowNameIndent < 0 && ind2 > flowsIndent)) {
        var fm = s.match(/^"?(implicit|password|clientCredentials|authorizationCode)"?\s*:/);
        if (fm) {
          flowName = fm[1]; flowNameIndent = ind2;
          if (flowName === 'implicit') add('oauth_implicit_flow', ln);
          if (flowName === 'password') add('oauth_password_flow', ln);
        }
      }
    }
    if (/^"?scopes"?\s*:\s*(\{\s*\}|)\s*,?$/.test(s)) {
      var nx = lines[n2 + 1];
      var emptyInline = /^"?scopes"?\s*:\s*\{\s*\}\s*,?$/.test(s);
      var nothingUnder = !nx || nx.trim() === '' || indentOf(nx) <= ind2;
      if (emptyInline || nothingUnder) add('oauth_scopes_empty', ln);
    }
    if (/^['"]?(\*|all|admin:\*|\*:\*|full_access)['"]?\s*:/.test(s) && schemesStart >= 0 && n2 > schemesStart && n2 < schemesEnd) {
      add('scope_wildcard', ln);
    }
    if (/^"?additionalProperties"?\s*:\s*true\s*,?$/.test(s)) add('additional_properties_open', ln);
    if (/^"?deprecated"?\s*:\s*true\s*,?$/.test(s)) deprecatedLines.push(ln);
    if (/^"?\/[^\s:"']*"?\s*:\s*\{?\s*,?$/.test(s) && /\/(debug|trace|actuator|internal|_dump|_debug|env)(\/|"|:|$)/i.test(s)) {
      add('internal_path_published', ln);
    }
    if (/^-?\s*"?(example|default)"?\s*:/.test(s) &&
        /(sk_live_[A-Za-z0-9]{6,}|ghp_[A-Za-z0-9]{16,}|github_pat_[A-Za-z0-9_]{20,}|AKIA[0-9A-Z]{12,}|xox[baprs]-[A-Za-z0-9-]{8,}|eyJ[A-Za-z0-9_-]{12,}\.[A-Za-z0-9_-]{8,}|BEGIN [A-Z ]*PRIVATE KEY)/.test(s)) {
      add('secret_in_example', ln);
    }
  }

  // ---- document-level ----
  if (hasPaths && !hasGlobalSecurity) add('no_global_security', pathsLine);
  if (serversSeen && !httpsServerSeen) add('no_https_server', 1);
  for (var d = 0; d < deprecatedLines.length; d++) {
    if (!sunsetSeen) add('deprecated_without_sunset', deprecatedLines[d]);
  }

  // ---- unused security schemes ----
  if (schemesStart >= 0) {
    var names = [];
    for (var k = schemesStart + 1; k < schemesEnd; k++) {
      var kl = lines[k];
      if (kl.trim() === '' || /^\s*#/.test(kl)) continue;
      if (indentOf(kl) === schemesIndent + 2) {
        var nm = kl.trim().match(/^"?([A-Za-z0-9_.-]+)"?\s*:/);
        if (nm) names.push({ name: nm[1], line: k + 1 });
      }
    }
    for (var q = 0; q < names.length; q++) {
      var re = new RegExp('^-?\\s*"?' + esc(names[q].name) + '"?\\s*:');
      var used = false;
      for (var z = 0; z < lines.length; z++) {
        if (z > schemesStart && z < schemesEnd) continue;
        if (re.test(lines[z].trim())) { used = true; break; }
      }
      if (!used) add('scheme_defined_unused', names[q].line, 'Scheme "' + names[q].name + '":');
    }
  }

  // ---- per-operation: missing 429 ----
  if (hasPaths) {
    for (var p = 0; p < lines.length; p++) {
      var pl = lines[p], ps = pl.trim();
      if (ps === '' || ps.charAt(0) === '#') continue;
      var om = ps.match(/^"?(get|post|put|patch|delete|head|options)"?\s*:\s*\{?\s*$/i);
      if (!om || indentOf(pl) <= pathsIndent) continue;
      var oEnd = blockRange(lines, p, indentOf(pl));
      var body = lines.slice(p, oEnd).join('\n');
      if (!/^\s*"?'?429'?"?\s*:/m.test(body)) add('missing_429', p + 1, 'Operation "' + om[1].toLowerCase() + '":');
    }
  }

  findings.sort(function (a, b) { return (a.line || 0) - (b.line || 0); });
  return { findings: findings };
}

var API = { engine: { check: check }, RULES: RULES, RULE_COUNT: RULES.length };
if (typeof module !== 'undefined' && module.exports) module.exports = API;
if (typeof window !== 'undefined') window.OASENGINE = API;
</script>
<script>// ⛔뼈대 — kit/scaffold.py 가 찍는다 (s143). findings [{check,sev,msg,line?}] → 글/HTML. 같은 파일이 Node 와 브라우저에서 산다.
'use strict';
function escapeHtml(s) { return String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;'); }
function summaryLine(res) {
  const f = (res && res.findings) || []; const e = f.filter(function (x) { return String(x.sev || 'error').toLowerCase().startsWith('err'); }).length;
  return f.length ? (f.length + ' finding' + (f.length === 1 ? '' : 's') + ' (' + e + ' error' + (e === 1 ? '' : 's') + ')') : 'Clean — no findings.';
}
function toText(res, label) {
  const f = (res && res.findings) || [];
  const lines = ['== ' + (label || '') + ' — ' + summaryLine(res)];
  f.forEach(function (x) { lines.push('  ' + String(x.sev || 'error').toUpperCase().slice(0, 4) + '  ' + (x.line ? 'L' + x.line + '  ' : '') + (x.check || x.id || '') + '  ' + (x.msg || x.message || '')); });
  return lines.join('\n');
}
function toHtml(res) {
  const f = (res && res.findings) || [];
  if (!f.length) return '<p class="ok">' + escapeHtml(summaryLine(res)) + '</p>';
  const rows = f.map(function (x) { return '<tr><td class="sev ' + escapeHtml(String(x.sev || 'error')) + '">' + escapeHtml(String(x.sev || 'error')) + '</td><td>' + (x.line ? 'L' + escapeHtml(x.line) : '') + '</td><td><code>' + escapeHtml(x.check || x.id || '') + '</code></td><td>' + escapeHtml(x.msg || x.message || '') + '</td></tr>'; }).join('');
  return '<p class="sum">' + escapeHtml(summaryLine(res)) + '</p><table><thead><tr><th>Level</th><th>Line</th><th>Check</th><th>What</th></tr></thead><tbody>' + rows + '</tbody></table>';
}
var OASREPORT = { escapeHtml: escapeHtml, summaryLine: summaryLine, toText: toText, toHtml: toHtml };
if (typeof module !== 'undefined' && module.exports) module.exports = OASREPORT;
else window.OASREPORT = OASREPORT;
</script>
<script>
(function(){
  var src=document.getElementById('src'), out=document.getElementById('out'), go=document.getElementById('go'), today=document.getElementById('today');
  today.value=new Date().toISOString().slice(0,10);
  function run(){ try { var res=window.OASENGINE.engine.check(src.value,{today:today.value}); out.innerHTML=window.OASREPORT.toHtml(res); } catch(e){ out.innerHTML='<p class="sev error">Could not run: '+String(e.message||e)+'</p>'; } }
  go.onclick=run; if (src.value.trim()) run();
})();
</script>


<section data-yj-install="free" style="margin:2rem auto 1rem;max-width:46rem;padding:1.25rem 1.25rem;border:1px solid #cfe3d6;border-radius:12px;background:#f6fbf8;font:16px/1.6 system-ui,-apple-system,Segoe UI,sans-serif"><h2 style="margin:0 0 .4rem;font-size:1.2rem">Install free</h2><p style="margin:0 0 .8rem;color:#41505f">Free for the file open in your editor - no key, no limit. The workspace sweep and the report ask for a key.</p><a href="https://www.npmjs.com/package/@readystack/openapi-security-contract-lint" rel="noopener" data-yj-install-door="npm" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'openapi-security-contract-lint',src:'npm',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">npm</a><a href="https://www.npmjs.com/package/@readystack/openapi-security-contract-lint" rel="noopener" data-yj-install-door="mcp" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'openapi-security-contract-lint',src:'mcp',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">MCP server (npx)</a><a href="https://hub.docker.com/r/getreadystack/openapi-security-contract-lint" rel="noopener" data-yj-install-door="docker" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'openapi-security-contract-lint',src:'docker',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">Docker Hub</a><pre style="margin:.5rem 0 0;padding:.6rem .8rem;background:#f3f5f8;border-radius:8px;font-size:.9rem;overflow:auto">npx @readystack/openapi-security-contract-lint &lt;file&gt;
npx @readystack/openapi-security-contract-lint --mcp
docker run --rm -v &quot;$PWD:/w&quot; getreadystack/openapi-security-contract-lint /w</pre><p style="margin:.6rem 0 0;font-size:.92rem;color:#41505f"><a href="https://getreadystack.com/api/buy/cl/polar_cl_l6iN1uWt0FwWu7tBsczD0jWpP2vxFM54Wdwqb3KPi1G" rel="nofollow" style="color:#0f7b3f;text-decoration:underline">Team? One key for every ReadyStack linter, 5 seats, $149 once</a></p></section>
<section data-yj-alert="rule" style="margin:1rem auto 1.5rem;max-width:46rem;padding:1.1rem 1.25rem;border:1px solid #d9dee6;border-radius:12px;background:#fbfcfd;font:15px/1.55 system-ui,-apple-system,Segoe UI,sans-serif"><b style="font-size:1.05rem">Get the full rule table as a PDF</b><div style="opacity:.78;margin:.35rem 0 .8rem">All 18 rules OpenAPI Security Contract Lint checks, each with its source and date, on printable pages you can hand to your team or auditor. We email it now, and once more only if a rule changes. No newsletter.</div><div style="display:flex;gap:8px;flex-wrap:wrap"><input id="yjae" type="email" placeholder="you@example.com" style="flex:1;min-width:210px;padding:11px 13px;border:1px solid rgba(140,150,165,.34);border-radius:8px;background:#fff;color:inherit"><button id="yjab" style="padding:11px 18px;border-radius:8px;border:0;cursor:pointer;background:#0f7b3f;color:#fff;font:600 15px system-ui,sans-serif">Email me the PDF</button></div><div id="yjao" hidden style="margin-top:.6rem;color:#0f7b3f">Sent. It arrives within the hour - check spam too.</div><script>(function(){var b=document.getElementById("yjab"),e=document.getElementById("yjae");if(!b||!e)return;b.addEventListener("click",function(){var v=e.value.trim();if(!/.+@.+\..+/.test(v))return;try{localStorage.setItem("yj_em","1");}catch(_){}fetch(location.origin+"/api/subscribe",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:v,slug:"openapi-security-contract-lint",lang:"en",source:"rule_table"+((b.closest&&b.closest("section")&&b.closest("section").getAttribute("data-yj-moved"))?"_at_result":"")})}).then(function(r){var o=document.getElementById("yjao");o.hidden=false;if(!r.ok){o.textContent="Could not save - please try again.";}}).catch(function(){var o=document.getElementById("yjao");o.hidden=false;o.textContent="Could not save - please try again.";});});})();</script><script>(function(){var e=document.getElementById("yjae"),s=e&&e.closest?e.closest("section"):null,o=document.getElementById("out");if(!s||!o)return;var d=false;function mv(ev){if(d)return;if(ev&&ev.target&&s.contains(ev.target))return;d=true;var b=o;while(b.parentNode&&b.parentNode!==document.body&&b.parentNode.tagName!=="MAIN"&&b.parentNode.tagName!=="ARTICLE")b=b.parentNode;if(!b.parentNode||b===s||b.contains(s))return;try{b.parentNode.insertBefore(s,b.nextSibling);s.setAttribute("data-yj-moved","1");}catch(_){}}document.addEventListener("input",mv,true);document.addEventListener("change",mv,true);})();</script></section>

<section data-yj-cta="paid" style="margin:3rem auto;max-width:46rem;padding:1.5rem 1.25rem;border:1px solid #d8dee9;border-radius:12px;background:#fbfcfe;font:16px/1.6 system-ui,-apple-system,Segoe UI,sans-serif"><h2 style="margin:0 0 .5rem;font-size:1.25rem">Get the complete version <span class="yj-price">$29</span></h2><p style="margin:.25rem 0 1rem;color:#41505f">This page is the working piece. The full pack has everything below.</p><p style="margin:0 0 1rem;color:#41505f">Finds the lines in an OpenAPI file that publish an endpoint with no authentication, an API key in the query string, or an OAuth grant RFC 9700 forbids.</p><p style="margin:0 0 1rem;color:#5b6b7c;font-size:.94rem">An application-security consultant reading the same contract by hand bills $150-$250 an hour</p><a href="https://getreadystack.com/api/buy/cl/polar_cl_nXjacvEwIBQ8xjkXZXDymfZbCc9cnushVFkc83YVTJW" rel="nofollow" style="display:inline-block;padding:.7rem 1.3rem;border-radius:8px;background:#1b64f2;color:#fff;text-decoration:none;font-weight:600">Buy the full version — $29</a></section>
<!--yj-share-s101--><div style="margin:28px 0 8px;text-align:center"><button id="yjcp" type="button" style="font:inherit;padding:10px 18px;border:1px solid #8884;border-radius:10px;background:transparent;cursor:pointer">Copy link</button><button id="yjimg" type="button" style="font:inherit;padding:10px 18px;margin-left:8px;border:1px solid #8884;border-radius:10px;background:transparent;cursor:pointer">Save result as image</button></div><script>(function(){var b=document.getElementById("yjimg");if(!b)return;b.addEventListener("click",function(){var o=document.getElementById("out");var txt=(o&&(o.innerText||o.textContent)||"").trim();if(!txt){b.textContent="(no result yet)";setTimeout(function(){b.textContent="Save result as image";},1500);return;}var lines=txt.split(String.fromCharCode(10)).filter(function(x){return x.trim();}).slice(0,10);var W=1200,P=56,c=document.createElement("canvas");c.width=W;var lh=38,H=P*2+70+lines.length*lh+60;c.height=H;var x=c.getContext("2d");x.fillStyle="#1a1512";x.fillRect(0,0,W,H);x.fillStyle="#f3e9d2";x.font="600 34px system-ui,Segoe UI,Arial";var t=(document.title||"").split("|")[0].trim();x.fillText(t.slice(0,54),P,P+34);x.font="22px ui-monospace,Menlo,Consolas,monospace";x.fillStyle="#e8dcc4";for(var i=0;i<lines.length;i++){x.fillText(lines[i].slice(0,88),P,P+70+i*lh+22);}x.font="600 22px system-ui,Segoe UI,Arial";x.fillStyle="#c9a227";var d=location.hostname.replace("www.","");x.fillText(d,W-P-x.measureText(d).width,H-P+6);c.toBlob(function(bl){var u=URL.createObjectURL(bl),a=document.createElement("a");a.href=u;a.download=(t||"result").replace(/[^a-z0-9]+/gi,"-").toLowerCase()+"-"+d+".png";document.body.appendChild(a);a.click();setTimeout(function(){URL.revokeObjectURL(u);a.remove();},2000);},"image/png");});})();</script><script>(function(){var b=document.getElementById("yjcp");if(!b)return;var t=b.textContent;b.addEventListener("click",function(){var u=location.href.split("#")[0];function d(){b.textContent="Link copied";setTimeout(function(){b.textContent=t;},1800);}if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(u).then(d,function(){prompt("",u);});}else{prompt("",u);}});})();</script><div class="yj-home-find"><div class="yj-lgbar"><a href="/" class="yj-lg on">EN</a><a href="/de/" class="yj-lg">DE</a><a href="/ja/" class="yj-lg">JA</a><a href="/es/" class="yj-lg">ES</a><a href="/pt/" class="yj-lg">PT</a></div><section class="yj-find"><h2>Find a tool</h2><input id="yjq" type="search" autocomplete="off" placeholder="Type a product name, format or task"><div id="yjr"></div></section><style>.yj-lgbar{position:absolute;top:14px;right:18px;display:flex;gap:6px;z-index:60}.yj-lg{font:600 12px/1 system-ui,sans-serif;padding:6px 9px;border-radius:4px;text-decoration:none;color:#8b96a3;border:1px solid rgba(140,150,165,.28)}.yj-lg:hover{color:#e8eaed;border-color:rgba(160,175,195,.6)}.yj-lg.on{color:#0e1116;background:#cfd6de;border-color:#cfd6de}.yj-find{max-width:760px;margin:34px auto 8px;padding:0 20px}.yj-find h2{font:600 20px/1.3 system-ui,sans-serif;margin:0 0 10px}#yjq{width:100%;box-sizing:border-box;padding:13px 15px;font:400 15.5px system-ui,sans-serif;border:1px solid rgba(140,150,165,.34);border-radius:8px;background:rgba(255,255,255,.04);color:inherit}#yjq:focus{outline:none;border-color:#7FD1C4}#yjr{margin-top:10px;display:flex;flex-direction:column;gap:6px}#yjr a{display:flex;justify-content:space-between;gap:12px;padding:11px 14px;border:1px solid rgba(140,150,165,.22);border-radius:6px;text-decoration:none;font:400 14.5px/1.4 system-ui,sans-serif;color:inherit}#yjr a:hover{border-color:#7FD1C4}#yjr .pr{font:600 13px system-ui,sans-serif;color:#5fbf9a;white-space:nowrap}</style><script>(function(){var q=document.getElementById("yjq"),r=document.getElementById("yjr"),D=null;function load(){if(D)return Promise.resolve(D);return fetch("/search-index.json").then(function(x){return x.json();}).then(function(j){D=j;return j;}).catch(function(){D=[];return D;});}function esc(t){return String(t).replace(/[&<>"]/g,function(c){return {"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c];});}function run(){var v=q.value.trim().toLowerCase();if(v.length<2){r.innerHTML="";return;}load().then(function(j){var w=v.split(/\s+/),o=[];for(var i=0;i<j.length&&o.length<12;i++){var t=j[i].t.toLowerCase();var ok=true;for(var k=0;k<w.length;k++){if(t.indexOf(w[k])<0){ok=false;break;}}if(ok)o.push(j[i]);}r.innerHTML=o.length?o.map(function(x){return "<a href=\""+esc(x.u)+"\">"+"<span>"+esc(x.t)+"</span>"+(x.p?"<span class=\"pr\">$"+x.p+"</span>":"")+"</a>";}).join(""):"";});}q.addEventListener("input",run);q.addEventListener("focus",load);})();</script></div>


<script data-yj="yj-view-beacon">(function(){try{var p=location.pathname;var k='yjv:'+p;if(sessionStorage.getItem(k))return;sessionStorage.setItem(k,'1');fetch('/api/ev',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({path:p,lang:"en",ref:(document.referrer||''),src:(function(){try{return (new URLSearchParams(location.search).get('ref')||'').replace(/[^a-z_]/g,'').slice(0,12)}catch(e){return ''}})()})}).catch(function(){});}catch(e){}})();</script>

<script data-yj="yj-use-beacon">(function(){try{var o=document.getElementById('out');if(!o)return;var p=location.pathname,k='yju:'+p,sent=false;function fire(){if(sent||sessionStorage.getItem(k))return;if(!(o.textContent||'').replace(/\s+/g,'').length)return;sent=true;sessionStorage.setItem(k,'1');fetch('/api/ev',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({path:p,lang:"en",t:'use',src:(function(){try{return (new URLSearchParams(location.search).get('ref')||'').replace(/[^a-z_]/g,'').slice(0,12)}catch(e){return ''}})()})}).catch(function(){});}var mo=new MutationObserver(function(){fire();if(sent)mo.disconnect();});mo.observe(o,{childList:true,characterData:true,subtree:true});}catch(e){}})();</script>
<section data-yj="yj-facts-v1" style="max-width:920px;margin:48px auto 0;padding:0 20px;font-family:system-ui,sans-serif"><style>.yj-article p{font:400 15.5px/1.7 system-ui,sans-serif;color:#26313b;margin:0 0 12px;max-width:72ch}.yj-article h2,.yj-article h3,.yj-article h4{font:600 17px/1.35 system-ui,sans-serif;color:#1b1f24;margin:22px 0 8px}.yj-article table{border-collapse:collapse;margin:10px 0 16px;font:14px/1.45 system-ui,sans-serif}.yj-article th,.yj-article td{border:1px solid #e2e6ea;padding:6px 10px;text-align:left}.yj-article th{background:#f4f6f8}.yj-article pre{background:#0f1720;color:#e6edf3;padding:12px 14px;border-radius:8px;overflow:auto;font:13px/1.5 ui-monospace,Menlo,monospace}.yj-article code{font:13px ui-monospace,Menlo,monospace;background:#f1f3f5;padding:1px 4px;border-radius:3px}.yj-article pre code{background:none;padding:0}.yj-article ul,.yj-article ol{font:15px/1.6 system-ui,sans-serif;color:#26313b}</style><p style="font:400 13px/1.5 system-ui,sans-serif;color:#6b7680;margin:0 0 14px"><time datetime="2026-09-13">Updated 2026-09-13</time> · ReadyStack</p><script type="application/ld+json">{"@context":"https://schema.org","@type":"WebPage","dateModified":"2026-09-13","author":{"@type":"Organization","name":"ReadyStack"}}</script><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:0 0 6px;color:#1b1f24">Worked example</h2><p style="font:400 14.5px/1.5 system-ui,sans-serif;margin:0 0 16px;color:#5b6570">Real numbers from this tool, line by line.</p><div class="yj-article"><p><img src="https://getreadystack.com/img/promo/sku55701_result_card.jpg" alt="OpenAPI Security Contract Lint" loading="lazy" style="max-width:100%;border-radius:6px"></p>
<p>Twenty-one findings in a 59-line OpenAPI file. That is what an API platform engineer in the EU gets back the first time they run OpenAPI Security Contract Lint on the contract their gateway already imports - eight errors, thirteen warnings, every one a line somebody wrote on purpose.</p>
<p>An OpenAPI document does not feel like production code. It sits in a docs folder and gets edited during a sprint nobody remembers. But it is the file the gateway imports, the file the SDK generator turns into client libraries you ship, and the file a reviewer asks for first. Whatever it declares becomes the contract, including the parts nobody meant to declare.</p>
<p>The fixture that ships with the extension is ordinary: two paths, three operations, the look of a spec somebody generated and then hand-edited. Here is what 18 rules find in it.</p>
<p>Line 6 is a server entry on http://. Every generated client takes that as the base URL, so credentials travel in the clear. Line 7 is where paths begins with no top-level security block above it - authentication has become opt-in per operation, so anything you forget ships open. Line 11 is the one that matters most: security: [] on a GET. In OpenAPI that is not a placeholder and not an omission. It states that the operation requires no authentication, and a gateway that imports the document publishes it exactly that way.</p>
<p>Line 25 is additionalProperties: true on a request schema - the mass-assignment route. Line 31 is an example carrying a live-looking key, and examples get copied into docs sites and SDK fixtures. Line 35 publishes a /debug path to external callers.</p>
<p>Then the security schemes. Line 45 declares an apiKey with in: query - that key is written into proxy access logs, browser history and Referer headers on every call. Line 49 is scheme: basic, a static credential replayed forever. Lines 53 and 56 are the two OAuth grants that RFC 9700 (BCP 240, January 2025) says clients MUST NOT use: the implicit grant, which returns the token in a URL fragment, and the resource owner password credentials grant. Line 55 is a flow with an empty scopes map - one all-or-nothing token. Two of the three declared schemes are never referenced at all.</p>
<p>Three operations document no 429 response, so nothing has an agreed throttling contract. One is deprecated with no sunset date named anywhere - the gap RFC 8594 exists to close.</p>
<p>All of it survives every free validator, because free validators ask a different question. Swagger Editor, JSON Schema tooling and Spectral&#x27;s stock ruleset check whether the document is well formed. A well-formed document can say security: [] on a payments endpoint all day long. Those tools have no opinion about that line. This one names it, cites the OWASP API Security Top 10 (2023) category, and points at the row number.</p>
<p>The reason to care now is that the paperwork caught up. Since 11 September 2026 the EU Cyber Resilience Act&#x27;s reporting clock runs: an actively exploited vulnerability in a product with digital elements sold into the EU is reported within 24 hours, a follow-up at 72 hours, a final report at 14 days. Your OpenAPI document describes the interface that gets found.</p>
<p>The free version audits the file open in your editor against all 18 rules and shows every finding, line number and rule reference. Nothing is withheld or counted down; that job finishes on its own. The $29 licence buys a different job: sweeping every spec in the workspace at once and writing a dated CSV, JSON or HTML report you keep next to the release. A consultant reading the same contract by hand bills $150 to $250 an hour.</p>
<p>The clean fixture, 63 lines, returns zero. The rules are not noise generators, and a document reviewed once passes.</p></div><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:34px 0 6px;color:#1b1f24">Questions people ask</h2><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What does OpenAPI Security Contract Lint actually do?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">It reads an OpenAPI document as text, line by line, against 18 rules taken from the OWASP API Security Top 10 (2023), RFC 9700 and RFC 8594. It reports the line number, the rule and the reason for each finding: endpoints declared with no authentication, API keys in the query string, forbidden OAuth grants, plaintext servers and undocumented rate limits.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Who is it for?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">API platform and backend engineers who hand an OpenAPI file to something that acts on it: an AWS or Azure gateway that imports it, an SDK generator that ships it to customers, or a security reviewer who asks for it before a release. It is aimed at teams shipping APIs into regulated European and US markets.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Why is a free validator not enough?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Swagger Editor, JSON Schema validators and Spectral&#x27;s stock ruleset check whether the document is well formed and internally consistent. A perfectly well-formed document can still say security: [] on a payments endpoint. Those tools have no opinion about that line; this one names it, cites the OWASP category, and points at the row number.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What is free and what needs a licence?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Free: audit the file open in your editor against all 18 rules, with every finding, line number and rule reference shown. Nothing is hidden or counted down. A licence adds a different job: sweeping every OpenAPI document in the workspace at once, and writing a dated CSV, JSON or HTML report you keep alongside the release.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What would this cost done by hand?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">An application-security consultant reading the same contract bills roughly $150 to $250 an hour, and the OAuth grant, scope and mass-assignment questions are the slow part of that hour. A full external API penetration test is a larger engagement again. The licence is $29 once,.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Why not just ask ChatGPT or another AI chat?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">A general AI chat answers from training data with a cutoff date, cannot read your repository and names no rule version. OpenAPI Security Contract Lint checks the file you open against 18 rules, each naming its legal or official basis, from a rule set dated 2026-09-20, and points at the exact line. For a filing, an audit or a client you need that dated result on your own files.</p></details><script type="application/ld+json" data-yj="yj-faq-ld">{"@context": "https://schema.org", "@type": "FAQPage", "mainEntity": [{"@type": "Question", "name": "What does OpenAPI Security Contract Lint actually do?", "acceptedAnswer": {"@type": "Answer", "text": "It reads an OpenAPI document as text, line by line, against 18 rules taken from the OWASP API Security Top 10 (2023), RFC 9700 and RFC 8594. It reports the line number, the rule and the reason for each finding: endpoints declared with no authentication, API keys in the query string, forbidden OAuth grants, plaintext servers and undocumented rate limits."}}, {"@type": "Question", "name": "Who is it for?", "acceptedAnswer": {"@type": "Answer", "text": "API platform and backend engineers who hand an OpenAPI file to something that acts on it: an AWS or Azure gateway that imports it, an SDK generator that ships it to customers, or a security reviewer who asks for it before a release. It is aimed at teams shipping APIs into regulated European and US markets."}}, {"@type": "Question", "name": "Why is a free validator not enough?", "acceptedAnswer": {"@type": "Answer", "text": "Swagger Editor, JSON Schema validators and Spectral's stock ruleset check whether the document is well formed and internally consistent. A perfectly well-formed document can still say security: [] on a payments endpoint. Those tools have no opinion about that line; this one names it, cites the OWASP category, and points at the row number."}}, {"@type": "Question", "name": "What is free and what needs a licence?", "acceptedAnswer": {"@type": "Answer", "text": "Free: audit the file open in your editor against all 18 rules, with every finding, line number and rule reference shown. Nothing is hidden or counted down. A licence adds a different job: sweeping every OpenAPI document in the workspace at once, and writing a dated CSV, JSON or HTML report you keep alongside the release."}}, {"@type": "Question", "name": "What would this cost done by hand?", "acceptedAnswer": {"@type": "Answer", "text": "An application-security consultant reading the same contract bills roughly $150 to $250 an hour, and the OAuth grant, scope and mass-assignment questions are the slow part of that hour. A full external API penetration test is a larger engagement again. The licence is $29 once,."}}, {"@type": "Question", "name": "Why not just ask ChatGPT or another AI chat?", "acceptedAnswer": {"@type": "Answer", "text": "A general AI chat answers from training data with a cutoff date, cannot read your repository and names no rule version. OpenAPI Security Contract Lint checks the file you open against 18 rules, each naming its legal or official basis, from a rule set dated 2026-09-20, and points at the exact line. For a filing, an audit or a client you need that dated result on your own files."}}]}</script><div id="yj-ask" style="margin:36px 0 0;padding:18px 18px 16px;border:1px solid #e2e6ea;border-radius:10px;background:#fbfbfc"><h2 style="font:600 19px/1.3 system-ui,sans-serif;margin:0 0 4px;color:#1b1f24">Ask about this tool</h2><p style="font:400 13.5px/1.5 system-ui,sans-serif;margin:0 0 12px;color:#5b6570">One question, answered by the person who built it. Your email only if you want the answer sent.</p><form onsubmit="return yjAsk(event)" style="display:grid;gap:8px"><textarea name="question" required minlength="10" maxlength="2000" rows="3" placeholder="Your question" style="font:14px/1.5 system-ui,sans-serif;padding:10px;border:1px solid #cfd6dd;border-radius:6px"></textarea><div style="display:flex;gap:8px;flex-wrap:wrap"><input type="email" name="email" placeholder="Email (optional)" style="flex:1;min-width:200px;font:14px system-ui,sans-serif;padding:9px 10px;border:1px solid #cfd6dd;border-radius:6px"><button type="submit" style="font:600 14px system-ui,sans-serif;padding:9px 18px;border:0;border-radius:6px;background:#1b1f24;color:#fff;cursor:pointer">Send</button></div><p id="yj-ask-msg" style="font:13px system-ui,sans-serif;margin:2px 0 0;color:#0a6238;min-height:1em"></p></form><script>function yjAsk(e){e.preventDefault();var f=e.target,m=document.getElementById("yj-ask-msg");m.style.color="#0a6238";fetch(location.origin+"/api/ask",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sku:55701,slug:"openapi-security-contract-lint",lang:"en",question:f.question.value,email:f.email.value,page:location.pathname})}).then(function(r){if(!r.ok)throw 0;m.textContent="Thanks \u2014 noted. Answers land on this page and, if you left an email, in your inbox.";f.question.value="";}).catch(function(){m.style.color="#a8551f";m.textContent="Could not send. Please try again in a minute.";});return false;}</script></div></section><section class="yj-related" style="max-width:920px;margin:56px auto 40px;padding:0 20px"><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:0 0 6px;color:#1b1f24">Same job, next tool</h2><p style="font:400 14.5px/1.5 system-ui,sans-serif;margin:0 0 20px;color:#5b6570">Free in the browser. The full version is a one-time licence.</p><div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:14px"><a class="yj-rel-c" href="https://getreadystack.com/tools/ingress-nginx-retirement-lint"><span class="yj-rel-t">Ingress-NGINX Retirement Lint</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/security-txt-cra-lint"><span class="yj-rel-t">security.txt Lint - RFC 9116 + CRA contact point</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/angular-removed-api-lint"><span class="yj-rel-t">Angular Removed-API Lint</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/bem-class-extractor-html"><span class="yj-rel-t">BEM Class Auditor for HTML and SCSS</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/cms-0057-api-gate"><span class="yj-rel-t">CMS-0057 API Gate</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/csaf-advisory-check-cra-2026"><span class="yj-rel-t">CSAF Advisory Check for CRA 2026</span><span class="yj-rel-p">$29</span></a></div><style>.yj-rel-c{display:flex;flex-direction:column;gap:8px;padding:15px 16px;border:1px solid #e2e6ea;border-radius:6px;text-decoration:none;background:#fff}.yj-rel-c:hover{border-color:#b9c2cb;background:#fafbfc}.yj-rel-t{font:500 14.5px/1.42 system-ui,sans-serif;color:#1b1f24}.yj-rel-p{font:600 13px system-ui,sans-serif;color:#0a6238}</style></section></body>
</html>