File size: 46,515 Bytes
7b8dc4a 346eed6 063aa54 346eed6 063aa54 346eed6 063aa54 346eed6 063aa54 346eed6 7b8dc4a | 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 | <!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Laravel Livewire security check — find browser reachable lines</title>
<meta name="description" content="This browser tool finds lines in Laravel Livewire and Blade files that the browser can reach, change, or call and names a fix for each.">
<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/livewire-client-surface-audit"></head>
<body>
<div class="wrap">
<h1>Laravel Livewire security check — find browser reachable lines</h1>
<p class="lede">Finds the lines in your Laravel Livewire and Blade files that the browser can reach, change or call, and names the fix on each one. Runs entirely in your browser — nothing is uploaded.</p>
<textarea id="src" spellcheck="false"><?php
namespace App\Livewire\Billing;
use Livewire\Attributes\Url;
use Livewire\Component;
use Livewire\WithFileUploads;
use App\Models\Invoice;
class InvoiceEditor extends Component
{
use WithFileUploads;
public $invoiceId;
public int $amountDue = 0;
public string $status = 'draft';
public $apiToken;
public $receipt;
#[Url]
public $customerId;
public function mount($invoiceId)
{
$this->invoiceId = $invoiceId;
$this->amountDue = Invoice::findOrFail($invoiceId)->amount_due;
}
public function approve()
{
Invoice::find($this->invoiceId)->update($this->only(['amountDue', 'status']));
}
public function deleteInvoice()
{
Invoice::find($this->invoiceId)->delete();
}
public function saveReceipt()
{
$path = $this->receipt->store('receipts');
Invoice::find($this->invoiceId)->receipt_path = $path;
}
public function render()
{
dump($this->apiToken);
return <<<'blade'
<div class="invoice">
{!! $invoiceNotes !!}
@include($layoutName)
<input type="number" wire:model.live="amountDue">
<button wire:click="approve">Approve</button>
</div>
blade;
}
}</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> Scan the open Livewire component or Blade file and list every client-reachable line with the fix, free and complete.. <strong>In the editor:</strong> Scan every PHP and Blade file in the workspace in one run and export a dated Markdown or JSON audit you can hand to a client or fail a CI build on. — that part asks for a licence key.</div>
<p class="note">Same engine as the VS Code extension, byte for byte.</p>
</div>
<script>var LWSURF_RULES = [
{
"id": "client_callable_method",
"sev": "high",
"title": "Public method is callable from the browser",
"msg": "Any public method on a Livewire component can be called by the browser as $wire.NAME(). This file contains no authorize(), Gate::, can() or abort_unless() call, so NAME() runs for anyone who can open the page.",
"fix": "Open the method with $this-\u003eauthorize('update', $model) (or Gate::authorize), or mark the method protected if the view does not call it."
},
{
"id": "unlocked_state_property",
"sev": "high",
"title": "Public property is writable from the browser",
"msg": "Livewire round-trips every public property through the page, so the browser can send back any value for $NAME before the next method runs.",
"fix": "Add the #[Livewire\\Attributes\\Locked] attribute above the property, or re-read the value from the database instead of trusting the round-trip."
},
{
"id": "secret_in_public_property",
"sev": "high",
"title": "Secret held in a public property",
"msg": "Public Livewire properties are serialized into the rendered HTML snapshot, so $NAME is readable in view-source by anyone who loads the page.",
"fix": "Move the value to a protected property, to session, or read it from config() inside the method that needs it."
},
{
"id": "unescaped_blade_echo",
"sev": "high",
"title": "Unescaped Blade echo",
"msg": "{!! !!} prints the value with no HTML escaping, so any markup or \u003cscript\u003e stored in that value runs in the visitor's browser.",
"fix": "Use {{ }} instead, or sanitize the value before printing it."
},
{
"id": "dynamic_template_include",
"sev": "medium",
"title": "Template name comes from a variable",
"msg": "@include/@component with a variable template name lets whatever fills that variable choose which view is rendered.",
"fix": "Name the view literally, or map the variable through a fixed allow-list array before including it."
},
{
"id": "url_bound_property",
"sev": "medium",
"title": "Query-string property feeds identity or state",
"msg": "#[Url] binds this property to the query string, so the value of $NAME is set by whatever the visitor types into the address bar.",
"fix": "Keep #[Url] for view state only. For an identifier, resolve the record through a policy or scope owned by the signed-in user.",
"lookahead": 3
},
{
"id": "unvalidated_upload",
"sev": "high",
"title": "Upload stored without validation",
"msg": "This component uses WithFileUploads and stores the file, but the file has no validate() or #[Validate] rule anywhere in the class, so the type and size of what lands on disk are the visitor's choice.",
"fix": "Call $this-\u003evalidate(['file' =\u003e 'required|file|mimes:pdf,png|max:2048']) before store()."
},
{
"id": "mount_without_authorization",
"sev": "high",
"title": "mount() loads a record with no ownership check",
"msg": "mount() receives its arguments from the page that renders the component and looks a record up, but the class has no authorize(), Gate::, can() or abort_unless() call, so any signed-in visitor can mount another tenant's record.",
"fix": "Add $this-\u003eauthorize('view', $record) in mount(), or scope the lookup to auth()-\u003euser()."
},
{
"id": "mass_assign_from_state",
"sev": "high",
"title": "Browser-held state written straight to the model",
"msg": "This line writes component state the browser can rewrite into the database in one call, so every unlocked public property on this class becomes a writable database column.",
"fix": "Write an explicit array of validated fields, e.g. $model-\u003eupdate($this-\u003evalidate()), and lock the properties that must not change."
},
{
"id": "debug_output_in_component",
"sev": "low",
"title": "Debug dump left in a rendered component",
"msg": "dd()/dump()/var_dump() prints the variable into the response, so whatever it holds is shown to whoever loads the page.",
"fix": "Remove the call, or replace it with Log::debug()."
}
]
;</script>
<script>/* Livewire Client Surface Audit — engine.
One file, two homes: Node (VS Code extension) and the browser (free web page). */
(function (root) {
'use strict';
var RULES = (typeof module !== 'undefined' && module.exports)
? require('./rules.json')
: root.LWSURF_RULES;
var BY_ID = {};
for (var i = 0; i < RULES.length; i++) BY_ID[RULES[i].id] = RULES[i];
// Livewire lifecycle + framework hooks: these are called by the framework, not by a user action.
var LIFECYCLE = /^(mount|boot|booted|render|hydrate|dehydrate|updating|updated|updatingQueryString|rules|messages|validationAttributes|placeholder|exception|__invoke|__construct)$/;
// Verbs that change or hand out data. A public method with one of these names is an action.
var ACTION_VERB = /^(delete|destroy|remove|purge|approve|reject|publish|unpublish|save|store|submit|update|edit|impersonate|promote|demote|grant|revoke|refund|cancel|reset|export|download|send|invite|activate|deactivate|archive|restore|pay|charge|assign|merge|import)/i;
var STATE_NAME = /(^|_)(id|ids)$|id$|role|price|amount|total|status|qty|quantity|discount|balance|credit|owner|tenant|admin|approved|verified|published|paid|rate|tax|plan|tier|limit|quota|permission/i;
var SECRET_NAME = /(token|secret|password|passwd|apikey|api_key|privatekey|private_key|webhook|signature|otp|pin|salt|cipher|credential)/i;
var RE_PROP = /^\s*(?:#\[[^\]]*\]\s*)?public\s+(?:readonly\s+)?(?:\??[A-Za-z_\\][A-Za-z0-9_\\|]*\s+)?\$([A-Za-z_][A-Za-z0-9_]*)/;
var RE_METHOD = /^\s*public\s+(?:static\s+)?function\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(/;
var RE_AUTH = /->authorize\s*\(|Gate::|->can\s*\(|@can\b|abort_unless\s*\(|abort_if\s*\(|->cannot\s*\(|Auth::user\(\)->can|policy\s*\(/;
var RE_VALID = /->validate\s*\(|->validateOnly\s*\(|\$rules\s*=|#\[Validate|Validator::make/;
var RE_LOOKUP = /::(find|findOrFail|firstWhere|where)\s*\(|::query\s*\(/;
var RE_LOCKED = /#\[\s*(?:Livewire\\Attributes\\)?Locked/;
var RE_URLATTR = /#\[\s*(?:Livewire\\Attributes\\)?Url\b/;
var RE_ECHO = /\{!!([\s\S]*?)!!\}/;
var RE_SAFE = /Purifier::|Str::markdown|->toHtml\s*\(|clean\s*\(/;
var RE_INCLUDE = /@(?:include|includeIf|includeWhen|component|extends)\s*\(\s*\$/;
var RE_STORE = /->(?:store|storeAs|storePublicly|storePubliclyAs)\s*\(/;
var RE_MASS = /->(?:update|fill|forceFill)\s*\(\s*\$this->|::(?:create|forceCreate|updateOrCreate)\s*\(\s*\$this->/;
var RE_DEBUG = /(^|[^A-Za-z0-9_$>])(dd|dump|var_dump|ray|print_r)\s*\(/;
var RE_UPLOADS = /WithFileUploads/;
var RE_COMMENT = /^\s*(?:\/\/|#[^\[]|\*|\/\*)/;
function finding(id, line, name) {
var r = BY_ID[id];
return {
check: id,
sev: r.sev,
line: line,
msg: (name ? r.msg.replace(/NAME/g, name) : r.msg.replace(/\$NAME/g, 'the property').replace(/NAME\(\)/g, 'the method')),
title: r.title,
fix: r.fix
};
}
function check(text, opts) {
text = String(text == null ? '' : text);
opts = opts || {};
var lines = text.split(/\r?\n/);
var out = [];
var claimed = {}; // line numbers already reported as a property
var hasAuth = RE_AUTH.test(text);
var hasValid = RE_VALID.test(text);
var hasUploads = RE_UPLOADS.test(text);
var hasLookup = RE_LOOKUP.test(text);
for (var i = 0; i < lines.length; i++) {
var ln = lines[i], no = i + 1;
if (RE_COMMENT.test(ln)) continue;
// --- properties -------------------------------------------------
var mp = ln.match(RE_PROP);
if (mp) {
var pname = mp[1];
var ctx = (lines[i - 1] || '') + '\n' + (lines[i - 2] || '') + '\n' + ln;
if (SECRET_NAME.test(pname)) {
out.push(finding('secret_in_public_property', no, pname));
claimed[no] = 1;
} else if (RE_URLATTR.test(ctx) && STATE_NAME.test(pname)) {
out.push(finding('url_bound_property', no, pname));
claimed[no] = 1;
} else if (STATE_NAME.test(pname) && !RE_LOCKED.test(ctx)) {
out.push(finding('unlocked_state_property', no, pname));
claimed[no] = 1;
}
}
// --- methods ----------------------------------------------------
var mm = ln.match(RE_METHOD);
if (mm) {
var fname = mm[1];
if (fname === 'mount' && hasLookup && !hasAuth) {
out.push(finding('mount_without_authorization', no, 'mount'));
} else if (!LIFECYCLE.test(fname) && ACTION_VERB.test(fname) && !hasAuth) {
out.push(finding('client_callable_method', no, fname));
}
}
// --- lines inside bodies and inline Blade ------------------------
var me = ln.match(RE_ECHO);
if (me && !RE_SAFE.test(ln)) out.push(finding('unescaped_blade_echo', no));
if (RE_INCLUDE.test(ln)) out.push(finding('dynamic_template_include', no));
if (hasUploads && !hasValid && RE_STORE.test(ln)) out.push(finding('unvalidated_upload', no));
if (RE_MASS.test(ln)) out.push(finding('mass_assign_from_state', no));
if (RE_DEBUG.test(ln)) out.push(finding('debug_output_in_component', no));
}
out.sort(function (a, b) { return a.line - b.line; });
return { findings: out };
}
var api = { engine: { check: check }, RULES: RULES, RULE_COUNT: RULES.length };
root.LWSURF_ENGINE = api;
if (typeof module !== 'undefined' && module.exports) module.exports = api;
})(typeof window !== 'undefined' ? window : globalThis);
</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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); }
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 LWSURF_REPORT = { escapeHtml: escapeHtml, summaryLine: summaryLine, toText: toText, toHtml: toHtml };
if (typeof module !== 'undefined' && module.exports) module.exports = LWSURF_REPORT;
else window.LWSURF_REPORT = LWSURF_REPORT;
</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.LWSURF_ENGINE.engine.check(src.value,{today:today.value}); out.innerHTML=window.LWSURF_REPORT.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://marketplace.visualstudio.com/items?itemName=ReadyStack.livewire-client-surface-audit" rel="noopener" data-yj-install-door="mp" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'livewire-client-surface-audit',src:'mp',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">VS Code Marketplace</a><a href="https://open-vsx.org/extension/readystack/livewire-client-surface-audit" rel="noopener" data-yj-install-door="ovsx" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'livewire-client-surface-audit',src:'ovsx',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">Open VSX (Cursor, VSCodium)</a><a href="https://www.npmjs.com/package/@readystack/livewire-client-surface-audit" rel="noopener" data-yj-install-door="npm" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'livewire-client-surface-audit',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/livewire-client-surface-audit" rel="noopener" data-yj-install-door="mcp" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'livewire-client-surface-audit',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/livewire-client-surface-audit" rel="noopener" data-yj-install-door="docker" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'livewire-client-surface-audit',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">ext install ReadyStack.livewire-client-surface-audit
npx @readystack/livewire-client-surface-audit <file>
npx @readystack/livewire-client-surface-audit --mcp</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 10 rules Livewire Client Surface Audit 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:"livewire-client-surface-audit",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">Marks every line of a Livewire or Blade file that the visitor's browser can call, rewrite or read</p><p style="margin:0 0 1rem;color:#5b6b7c;font-size:.94rem">A single-application web penetration test typically starts around $2,000.</p><a href="https://getreadystack.com/api/buy/cl/polar_cl_CeX6hwvCZRApL28KFlpqWeqOIttcxlAsNaLxA23WFNT" 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 {"&":"&","<":"<",">":">",'"':"""}[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-14">Updated 2026-09-14</time> · ReadyStack</p><script type="application/ld+json">{"@context":"https://schema.org","@type":"WebPage","dateModified":"2026-09-14","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/sku62908_result_card.jpg" alt="Livewire Client Surface Audit" loading="lazy" style="max-width:100%;border-radius:6px"></p>
<p>Fourteen reachable lines in one 58-line Livewire component. That is the sample billing screen shipped with this extension, and it is the shape of the component Laravel agencies and product teams review every week.</p>
<p>Livewire 3 quietly redefines two PHP keywords. In plain PHP, <code>public</code> means "other PHP can reach this". In a Livewire component it means something larger: every public property is serialized into the page, round-tripped through the browser and sent back on the next request, and every public method can be invoked from the browser as <code>$wire.methodName()</code>. The template does not have to mention either one. <code>public function deleteInvoice()</code> is an endpoint. <code>public $amountDue</code> is a form field.</p>
<p>That is the gap AI assistants fall into. An assistant asked for "a Livewire component that approves an invoice" writes idiomatic PHP: public properties for the state, public methods for the actions, authorization left for later. The result compiles, renders, passes type checks and ships.</p>
<p>Run the audit over the sample component and ten rules produce fourteen findings: eleven high, two medium, one low. <code>public $invoiceId</code> at line 14 has no <code>#[Locked]</code>, so the browser chooses which invoice the next method touches. <code>public $apiToken</code> at line 17 is serialized into the HTML snapshot and readable in view-source. <code>mount()</code> at line 23 looks a record up and the class never calls <code>authorize()</code>, <code>Gate::</code>, <code>can()</code> or <code>abort_unless()</code> — so any signed-in visitor can mount another tenant's invoice. <code>approve()</code>, <code>deleteInvoice()</code> and <code>saveReceipt()</code> at lines 29, 34 and 39 are three callable endpoints on that same unguarded class. Line 31 writes <code>$this->only(...)</code> straight into the row. Line 41 stores an upload from a class with no validation rule. Line 51 prints <code>{!! $invoiceNotes !!}</code> unescaped, and line 52 includes a template whose name comes from a variable.</p>
<p>The fixed twin of that file — same class, 65 lines, <code>#[Locked]</code> on the state, <code>authorize()</code> in <code>mount()</code> and in each action, a <code>validate()</code> call before <code>store()</code>, <code>{{ }}</code> instead of <code>{!! !!}</code> — returns zero.</p>
<p>Nothing here needs a novel analysis. It needs a rule set that knows what <code>public</code> means inside Livewire, which is exactly what a general PHP linter does not carry. PHPStan, Psalm and Larastan model the call graph as PHP defines it; the browser is not a caller in that graph. A chatbot can explain the round-trip in a paragraph but cannot walk the files in your repository.</p>
<p>The clock matters as much as the finding. The moment an unguarded public method hands one tenant another tenant's record, that is a personal-data breach, and GDPR Article 33 gives the controller seventy-two hours from awareness to notify the supervisory authority. The finding you did not run is the one that starts that timer without telling you.</p>
<p>The free tier is the whole job for one file: open any <code>.php</code> or <code>.blade.php</code> file, run the check, and every finding from all ten rules comes back with its line number and its fix. No watermark, no counter, nothing held behind a key. The same engine runs as a single web page, so a reviewer with no editor can paste a component in and read the same result.</p>
<p>What the full version adds is a different axis, not a bigger slice of the same one: it sweeps every PHP and Blade file in the workspace in one run and exports a dated Markdown or JSON audit — the artefact you attach to a release, hand to a client, or fail a build on. Yardstick: a single-application web penetration test typically starts around $2,000.</p></div><h2 style="font:600 19px/1.3 system-ui,sans-serif;margin:30px 0 10px;color:#1b1f24">15 seconds — what it actually does</h2><video controls muted playsinline preload="none" poster="/img/promo/sku62908_result_card.jpg" style="width:100%;max-width:360px;border-radius:10px;display:block;margin:0 auto"><source src="/media/sku62908_short.mp4" type="video/mp4"></video><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 Livewire Client Surface Audit actually do?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">It reads the PHP or Blade file you have open and marks every line the visitor's browser can reach: public methods callable as $wire.name(), public properties the browser round-trips and can rewrite, secrets serialized into the page HTML, unvalidated uploads, unescaped Blade echoes. Ten rules, and each finding carries the line number and the one-line fix.</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 this for?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Laravel teams shipping Livewire 3 components: agencies handing an app to a client, product teams reviewing components an AI assistant wrote, and contractors answering a customer security questionnaire. One unguarded public method that hands over another tenant's record is a breach with a 72-hour GDPR Article 33 reporting clock attached to it.</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 will PHPStan or a chatbot not do this?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">PHPStan, Psalm and Larastan model public the way PHP defines it, as reachable from other PHP. Livewire redefines public as reachable from the visitor's browser, and no general PHP linter ships a rule for that round-trip. A chatbot can explain the idea but cannot walk your files. The sample component here returns fourteen findings across fifty-eight lines.</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 is paid?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Free covers one file completely: open any .php or .blade.php file, run the check, and you get all ten rules, every finding, the line number and the fix. No watermark, no counter, no locked result. The full version changes the axis instead: it sweeps the whole workspace at once and exports a dated Markdown or JSON audit.</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 does the alternative cost?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">A single-application web penetration test typically starts around $2,000, and a security questionnaire answered by a senior Laravel contractor costs a day of that contractor's rate. The full version is $29 once, one licence key per person or team seat,. The free tier stays free and needs no key at all.</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. Livewire Client Surface Audit checks the file you open against 10 rules from a rule set dated 2026-09-20, and points at the exact line with the fix. 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 Livewire Client Surface Audit actually do?", "acceptedAnswer": {"@type": "Answer", "text": "It reads the PHP or Blade file you have open and marks every line the visitor's browser can reach: public methods callable as $wire.name(), public properties the browser round-trips and can rewrite, secrets serialized into the page HTML, unvalidated uploads, unescaped Blade echoes. Ten rules, and each finding carries the line number and the one-line fix."}}, {"@type": "Question", "name": "Who is this for?", "acceptedAnswer": {"@type": "Answer", "text": "Laravel teams shipping Livewire 3 components: agencies handing an app to a client, product teams reviewing components an AI assistant wrote, and contractors answering a customer security questionnaire. One unguarded public method that hands over another tenant's record is a breach with a 72-hour GDPR Article 33 reporting clock attached to it."}}, {"@type": "Question", "name": "Why will PHPStan or a chatbot not do this?", "acceptedAnswer": {"@type": "Answer", "text": "PHPStan, Psalm and Larastan model public the way PHP defines it, as reachable from other PHP. Livewire redefines public as reachable from the visitor's browser, and no general PHP linter ships a rule for that round-trip. A chatbot can explain the idea but cannot walk your files. The sample component here returns fourteen findings across fifty-eight lines."}}, {"@type": "Question", "name": "What is free and what is paid?", "acceptedAnswer": {"@type": "Answer", "text": "Free covers one file completely: open any .php or .blade.php file, run the check, and you get all ten rules, every finding, the line number and the fix. No watermark, no counter, no locked result. The full version changes the axis instead: it sweeps the whole workspace at once and exports a dated Markdown or JSON audit."}}, {"@type": "Question", "name": "What does the alternative cost?", "acceptedAnswer": {"@type": "Answer", "text": "A single-application web penetration test typically starts around $2,000, and a security questionnaire answered by a senior Laravel contractor costs a day of that contractor's rate. The full version is $29 once, one licence key per person or team seat,. The free tier stays free and needs no key at all."}}, {"@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. Livewire Client Surface Audit checks the file you open against 10 rules from a rule set dated 2026-09-20, and points at the exact line with the fix. 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:62908,slug:"livewire-client-surface-audit",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/n8n-social-media-workflow-agency-content-approval"><span class="yj-rel-t">n8n Social Media Workflow for Agency Content Approval — 6 Client Sign-Off Auto</span><span class="yj-rel-p">$59</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/email-footer-law-lint"><span class="yj-rel-t">Email Footer Law Lint</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/firmware-release-gate"><span class="yj-rel-t">Firmware Release Gate for sdkconfig and prj.conf</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/play-release-blocker-lint"><span class="yj-rel-t">Google Play Release Blocker Lint - target API 36, Billing 8, 16 KB</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/livewire3-upgrade-blocker-lint"><span class="yj-rel-t">Livewire 3 Upgrade Blocker Lint</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/otel-collector-drift-lint"><span class="yj-rel-t">OTel Collector Drift Lint</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>
|