diff --git a/dev/design/TAINT_MODE.md b/dev/design/TAINT_MODE.md index 6593e7d9a..59023dd1f 100644 --- a/dev/design/TAINT_MODE.md +++ b/dev/design/TAINT_MODE.md @@ -6,11 +6,30 @@ Perl's taint mode (`-T` flag) tracks data from external sources (environment var ## Requirements -1. **No extra storage for normal scalars** - RuntimeScalar size must not increase -2. **No extra runtime checks for normal scalars** - Only tainted scalars incur overhead -3. **Gradual implementation** - Each phase delivers working functionality +1. **Low overhead** - Taint metadata is a single boolean on `RuntimeScalar` +2. **Centralized policy** - Sources, propagation, and dangerous-operation checks use shared helpers +3. **Backend parity** - JVM and interpreter execution must preserve the same taint metadata +4. **Gradual implementation** - Each phase delivers tested functionality -## Design: TAINTED Type (Wrapper Pattern) +## Design: Scalar Taint Flag + +> **Decision (2026-08-09):** The wrapper proposal below was superseded by the +> field-based implementation already present in the runtime. A wrapper adds a +> new scalar type that every value-access fast path must understand; the boolean +> flag composes with tied, read-only, special-variable, and reference scalars +> without changing their existing type. + +`RuntimeScalar` owns a `boolean tainted` field. Copies and assignments preserve +the flag, operations call `propagateTaint(...)`, and external sources call +`taintFromExternalInput()`. `isTainted()` resolves special, tied, and read-only +scalars before inspecting the flag. Security-sensitive operations call the +central `RuntimeScalar.checkTaint(value, operation)` helper, which only enforces +the flag when the current thread is running under `-T`. + +The original wrapper sketch is retained below as historical context, not as an +implementation target. + +### Rejected Alternative: TAINTED Type (Wrapper Pattern) Add a `TAINTED` type to RuntimeScalarType, following the existing TIED_SCALAR pattern: @@ -132,6 +151,11 @@ public String toString() { **Goal:** Add TAINTED type and basic taint detection. +**Implemented differently:** source marking and detection use the scalar taint +flag described above. `$^X`, `%ENV`, `@ARGV`, file reads, `read`, and directory +reads are tainted while `-T` is active. `Scalar::Util::tainted()` and +`builtin::is_tainted()` query `RuntimeScalar.isTainted()`. + ### Changes 1. **Add TAINTED constant to RuntimeScalarType.java:** @@ -190,6 +214,11 @@ public String toString() { **Goal:** Taint propagates through assignment and operations. +**Implemented differently:** constructors and `set()` copy the boolean flag. +Concatenation, interpolation/join, substring lvalues, the primary arithmetic +operators and numeric functions, `length`, case conversion, `ord`, `oct`, and +`hex` preserve taint. Further operator coverage remains an explicit audit item. + ### Changes 1. **Update set() to propagate taint:** @@ -280,7 +309,7 @@ public String toString() { ```java // Helper method public static void checkTaint(RuntimeScalar scalar, String operation) { - if (scalar.isTainted()) { + if (GlobalContext.isTaintModeActive() && scalar.isTainted()) { throw new PerlCompilerException( "Insecure dependency in " + operation + " while running with -T switch" ); @@ -321,6 +350,8 @@ RuntimeScalar capture = new RuntimeScalar(matchedText); // The captured value is untainted regardless of source ``` +This behavior is implemented and covered by the taint regression test. + --- ## Files to Modify by Phase @@ -351,37 +382,12 @@ RuntimeScalar capture = new RuntimeScalar(matchedText); ## Cleanup -After implementing the TAINTED type approach: -- Remove `RuntimeScalarTaint.java` (no longer needed) -- Remove any WeakHashMap-based taint tracking code +The rejected wrapper and WeakHashMap approaches were not introduced. There is +no `RuntimeScalarTaint.java` cleanup required. --- -## Progress Tracking - -### Current Status: Phase 1 complete - -### Completed Phases - -- [x] **Phase 1: Minimal Fix for IPC::System::Simple** (2026-03-24) - - Modified `src/main/perl/lib/IPC/System/Simple.pm` `_check_taint()` to block ALL external commands when `${^TAINT}` is set - - Added `isTainted()` method to RuntimeScalar.java (returns false, ready for Phase 2) - - Updated `ScalarUtil.tainted()` to use `isTainted()` method - - **Bonus fix**: Reset `$?` to 0 before END blocks in SpecialBlock.java (Perl semantics) - this fixed spurious "Looks like your test exited with X" warnings from Test::Builder - - **Test results**: IPC::System::Simple 15/17 test programs pass, 169/181 subtests (93%) - -### Infrastructure Complete -- [x] `-T` flag parsing -- [x] `${^TAINT}` variable -- [x] `isTainted()` method stub - -### Next Steps (Phase 2) -1. Add TAINTED type constant to RuntimeScalarType.java -2. Implement `taint()` and `getActualScalar()` methods -3. Mark `$^X`, `%ENV`, `@ARGV` as tainted sources -4. Update `tainted()` to return true for TAINTED type +## Implementation Tracking -### Open Questions -- Should @ARGV be tainted? (Yes in Perl) -- Handle taint in hash/array element access? -- Taint and references - should $$ref propagate taint? +Implementation progress and test results are tracked in the commits and draft +pull request rather than maintained as a second change log in this document. diff --git a/docs/about/changelog.md b/docs/about/changelog.md index 412caf7a6..1cbc33ae9 100644 --- a/docs/about/changelog.md +++ b/docs/about/changelog.md @@ -4,6 +4,10 @@ Release history of PerlOnJava. See [Roadmap](roadmap.md) for future plans. ## Work in progress +- Add Perl taint mode with `-T` on both JVM and interpreter backends, including + external-input provenance, scalar and regex propagation, capture-based + untainting, and security checks for process execution, code loading, file + mutation, and other sensitive operations. - Bugfix: targeted weak-reference sweeps preserve objects rescued by `DESTROY` until rescue-specific reachability cleanup runs, keeping live DBIx::Class storage callbacks valid after a schema self-rescue. diff --git a/docs/reference/feature-matrix.md b/docs/reference/feature-matrix.md index 920baa173..60ed9ea92 100644 --- a/docs/reference/feature-matrix.md +++ b/docs/reference/feature-matrix.md @@ -45,6 +45,7 @@ PerlOnJava implements most core Perl features with some key differences: - DBI with JDBC integration - Subroutine prototypes - Tied variables +- Taint mode (`-T`) - Method Resolution Order 🚧 Partially Supported: @@ -138,10 +139,9 @@ The built-in Perl debugger (`perl -d`) provides interactive debugging. See [Debu - ✅ UTF-16 is accepted in source code. - ✅ Accept command line switches from the shebang line. -- ✅ Accept command line switches: `-c`, `-e`, `-E`, `-p`, `-n`, `-i`, `-I`, `-0`, `-a`, `-d`, `-f`, `-F`, `-m`, `-M`, `-g`, `-l`, `-h`, `-s`, `-S`, `-x`, `-v`, `-V`, `-?`, `-w`, `-W`, `-X` are implemented. +- ✅ Accept command line switches: `-c`, `-e`, `-E`, `-p`, `-n`, `-i`, `-I`, `-0`, `-a`, `-d`, `-f`, `-F`, `-m`, `-M`, `-g`, `-l`, `-h`, `-s`, `-S`, `-T`, `-x`, `-v`, `-V`, `-?`, `-w`, `-W`, `-X` are implemented. - ❌ Missing command line switches include: - - `-T`: Taint checks. - - `-t`: Taint checks with warnings. + - `-t`: Taint checks with warnings. The option is accepted, but warning-mode taint semantics are not implemented. - `-u`: Dumps core after compiling. - `-U`: Allows unsafe operations. - `-D[number/list]`: Sets debugging flags. @@ -213,7 +213,10 @@ my @copy = @{$z}; # ERROR - ✅ **Typeglob as hash**: `*$val{$k}` for `SCALAR`, `ARRAY`, `HASH`, `CODE`, `IO` is implemented. - ✅ **Use string as a scalar reference**: Support for scalar references from strings is implemented. - ✅ **Tied Scalars**: Support for tying scalars to classes is implemented. See also [Tied Arrays](#arrays-hashes-and-lists), [Tied Hashes](#arrays-hashes-and-lists), [Tied Handles](#io-operations). -- ❌ **Taint checks**: Support for taint checks is not implemented. +- ✅ **Taint checks**: `-T` marks external inputs, propagates taint through + scalar and regular-expression operations, supports capture-based untainting, + and rejects tainted values at security-sensitive operations. Supported by + both JVM and interpreter backends. - ❌ **`local` special cases**: `local *HANDLE = *HANDLE` doesn't create a new typeglob. - 🚧 **Variable attributes**: `my $x : attr` supported via `MODIFY_SCALAR_ATTRIBUTES` etc. diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index a2dcc422c..9df5c8e69 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -1238,6 +1238,7 @@ public void visit(BlockNode node) { // valid target for unlabeled last/next/redo (matches JVM // EmitBlock's pushLoopLabels(... isBareBlock, isBareBlock)). blockLoopInfo = new LoopInfo(node.labelName, blockLoopStartPc, true); + blockLoopInfo.resultReg = outerResultReg; loopStack.push(blockLoopInfo); } @@ -6908,11 +6909,12 @@ Map getCapturedVarIndices() { * Extracted for use by CompileOperator. */ void handleLoopControlOperator(OperatorNode node, String op) { + boolean implicitGivenLast = node.getBooleanAnnotation("implicitGivenLast"); // Extract label if present String labelStr = null; boolean isDynamicLabel = false; int dynamicLabelReg = -1; - if (node.operand instanceof ListNode labelNode && !labelNode.elements.isEmpty()) { + if (!implicitGivenLast && node.operand instanceof ListNode labelNode && !labelNode.elements.isEmpty()) { Node arg = labelNode.elements.getFirst(); if (arg instanceof IdentifierNode) { labelStr = ((IdentifierNode) arg).name; @@ -7045,6 +7047,22 @@ void handleLoopControlOperator(OperatorNode node, String op) { throwCompilerException("Can't \"" + op + "\" outside a loop block", node.getIndex()); } + // Preserve the final expression of a when clause as the result of its + // enclosing given block before jumping to that block's end. + if (implicitGivenLast) { + Object resultAnnotation = node.getAnnotation("implicitGivenResult"); + Node result = resultAnnotation instanceof Node ? (Node) resultAnnotation : null; + if (result != null) { + compileNode(result, -1, RuntimeContextType.SCALAR); + if (targetLoop.resultReg >= 0 && lastResultReg >= 0) { + emitAliasWithTarget(targetLoop.resultReg, lastResultReg); + } + } else if (targetLoop.resultReg >= 0) { + emit(Opcodes.LOAD_UNDEF); + emitReg(targetLoop.resultReg); + } + } + // Emit the opcode and record the PC to be patched later short opcode = op.equals("last") ? Opcodes.LAST : op.equals("next") ? Opcodes.NEXT @@ -7078,6 +7096,7 @@ private static class LoopInfo { final boolean isTrueLoop; // True for for/while/foreach; false for do-while/bare blocks int continuePc; // PC for next (continue block or increment) int cleanupScopeIndex; // Lower bound for scopes bypassed by local loop control + int resultReg; // Result register for value-producing synthetic blocks LoopInfo(String label, int startPc, boolean isTrueLoop) { this.label = label; @@ -7085,6 +7104,7 @@ private static class LoopInfo { this.isTrueLoop = isTrueLoop; this.continuePc = -1; // Will be set later this.cleanupScopeIndex = -1; + this.resultReg = -1; this.breakPcs = new ArrayList<>(); this.nextPcs = new ArrayList<>(); this.redoPcs = new ArrayList<>(); diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index 95fd4bee3..4f075de9e 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -39,12 +39,7 @@ static RuntimeScalar ensureMutableScalar(RuntimeBase val) { } if (val instanceof ScalarSpecialVariable sv) { RuntimeScalar src = sv.getValueAsScalar(); - RuntimeScalar copy = new RuntimeScalar(); - copy.type = src.type; - copy.value = src.value; - copy.numericLiteralText = src.numericLiteralText; - copy.numericContextSeen = src.numericContextSeen; - return copy; + return new RuntimeScalar(src); } return (RuntimeScalar) val; } diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java index 2ef986698..82f2684f1 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java @@ -620,7 +620,19 @@ private static void visitSimpleUnaryWithDefault(BytecodeCompiler bc, OperatorNod private static void visitGenericListOpCase(BytecodeCompiler bc, OperatorNode node, short opcode) { int argsReg; if (node.operand != null) { - bc.compileNode(node.operand, -1, RuntimeContextType.LIST); + boolean commandWithHandle = (opcode == Opcodes.SYSTEM || opcode == Opcodes.EXEC) + && node.operand instanceof ListNode list && list.handle != null; + ListNode commandArgs = commandWithHandle ? (ListNode) node.operand : null; + if (commandWithHandle) { + commandArgs.elements.addFirst(commandArgs.handle); + } + try { + bc.compileNode(node.operand, -1, RuntimeContextType.LIST); + } finally { + if (commandWithHandle) { + commandArgs.elements.removeFirst(); + } + } int operandReg = bc.lastResultReg; argsReg = bc.allocateRegister(); bc.emit(Opcodes.SCALAR_TO_LIST); diff --git a/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java b/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java index 70049fb29..a3a995541 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java @@ -185,12 +185,9 @@ public static RuntimeList evalStringList(RuntimeScalar codeScalar, int siteStrictOptions, int siteFeatureFlags, boolean isEvalbytes) { - try { - RuntimeCode.rejectTaintedEval(codeScalar); - } catch (PerlCompilerException e) { - WarnDie.catchEval(e); - return new RuntimeList(new RuntimeScalar()); - } + // The eval whose source is tainted cannot catch its own security error. + // Let an enclosing eval block handle it, matching the JVM backend and Perl. + RuntimeCode.rejectTaintedEval(codeScalar); return evalStringList(codeScalar.toString(), codeScalar.type, currentCode, registers, sourceName, sourceLine, callContext, siteRegistry, siteStrictOptions, siteFeatureFlags, isEvalbytes); } diff --git a/src/main/java/org/perlonjava/backend/bytecode/InlineOpcodeHandler.java b/src/main/java/org/perlonjava/backend/bytecode/InlineOpcodeHandler.java index 517e671f5..e40989c61 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/InlineOpcodeHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/InlineOpcodeHandler.java @@ -44,12 +44,7 @@ static RuntimeScalar ensureMutableScalar(RuntimeBase val) { } if (val instanceof ScalarSpecialVariable sv) { RuntimeScalar src = sv.getValueAsScalar(); - RuntimeScalar copy = new RuntimeScalar(); - copy.type = src.type; - copy.value = src.value; - copy.numericLiteralText = src.numericLiteralText; - copy.numericContextSeen = src.numericContextSeen; - return copy; + return new RuntimeScalar(src); } return (RuntimeScalar) val; } @@ -541,7 +536,7 @@ public static int executeArrayGet(int[] bytecode, int pc, RuntimeBase[] register RuntimeScalar idx = (RuntimeScalar) registers[indexReg]; if (arrayBase instanceof RuntimeArray arr) { - registers[rd] = arr.get(idx.getInt()); + registers[rd] = arr.get(idx); } else if (arrayBase instanceof RuntimeList list) { int index = idx.getInt(); if (index < 0) index = list.elements.size() + index; @@ -570,7 +565,7 @@ public static int executeArraySet(int[] bytecode, int pc, RuntimeBase[] register RuntimeBase valueBase = registers[valueReg]; RuntimeScalar val = (valueBase instanceof RuntimeScalar) ? (RuntimeScalar) valueBase : valueBase.scalar(); - RuntimeScalar element = arr.get(idx.getInt()); + RuntimeScalar element = arr.get(idx); element.set(val); registers[rd] = element; return pc; @@ -741,10 +736,16 @@ public static int executeHashSet(int[] bytecode, int pc, RuntimeBase[] registers return pc; } - RuntimeScalar copy = new RuntimeScalar(); - val.addToScalar(copy); - hash.put(key.toString(), copy); - registers[rd] = copy; + if (hash.type == RuntimeHash.TIED_HASH) { + RuntimeScalar element = hash.get(key); + element.set(val); + registers[rd] = element; + } else { + RuntimeScalar copy = new RuntimeScalar(); + val.addToScalar(copy); + hash.put(key.toString(), copy); + registers[rd] = copy; + } return pc; } diff --git a/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java b/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java index 0e300b011..dc4a78ba9 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java @@ -1285,9 +1285,7 @@ public static int executeLength( RuntimeBase stringBase = registers[stringReg]; RuntimeScalar stringScalar = stringBase.scalar(); - String str = stringScalar.toString(); - int length = str.codePointCount(0, str.length()); - registers[rd] = new RuntimeScalar(length); + registers[rd] = StringOperators.length(stringScalar); return pc; } diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitControlFlow.java b/src/main/java/org/perlonjava/backend/jvm/EmitControlFlow.java index ef9bc865a..dcf981bf0 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitControlFlow.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitControlFlow.java @@ -92,7 +92,8 @@ private static void emitGotoArgsArray(EmitterVisitor emitterVisitor, Node argsNo * @param node The operator node representing the control flow statement * @throws PerlCompilerException if the operator is used outside a loop block */ - static void handleNextOperator(EmitterContext ctx, OperatorNode node) { + static void handleNextOperator(EmitterVisitor emitterVisitor, OperatorNode node) { + EmitterContext ctx = emitterVisitor.ctx; if (CompilerOptions.DEBUG_ENABLED) ctx.logDebug("visit(next)"); String operator = node.operator; @@ -199,9 +200,24 @@ static void handleNextOperator(EmitterContext ctx, OperatorNode node) { return; } + // A when-clause's implicit last carries the clause's final value out + // of the synthetic given loop. Evaluate it explicitly in scalar + // context and leave it on the operand stack for the given block's + // result. Ordinary last remains valueless and follows the path below. + boolean implicitGivenLast = node.getBooleanAnnotation("implicitGivenLast"); + if (implicitGivenLast) { + Object resultAnnotation = node.getAnnotation("implicitGivenResult"); + Node result = resultAnnotation instanceof Node ? (Node) resultAnnotation : null; + if (result != null) { + result.accept(emitterVisitor.with(RuntimeContextType.SCALAR)); + } else { + EmitOperator.emitUndef(ctx.mv); + } + } + // Handle return values based on context if (loopLabels.context != RuntimeContextType.VOID) { - if (operator.equals("next") || operator.equals("last")) { + if ((operator.equals("next") || operator.equals("last")) && !implicitGivenLast) { // For non-void contexts, ensure an 'undef' value is pushed to maintain stack consistency EmitOperator.emitUndef(ctx.mv); } diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitOperatorNode.java b/src/main/java/org/perlonjava/backend/jvm/EmitOperatorNode.java index 17aecfefe..95658498d 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitOperatorNode.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitOperatorNode.java @@ -54,7 +54,7 @@ public static void emitOperatorNode(EmitterVisitor emitterVisitor, OperatorNode case "our", "state", "my" -> EmitVariable.handleMyOperator(emitterVisitor, node); // Control flow - case "next", "redo", "last" -> EmitControlFlow.handleNextOperator(emitterVisitor.ctx, node); + case "next", "redo", "last" -> EmitControlFlow.handleNextOperator(emitterVisitor, node); case "return" -> EmitControlFlow.handleReturnOperator(emitterVisitor, node); case "goto" -> EmitControlFlow.handleGotoLabel(emitterVisitor, node); diff --git a/src/main/java/org/perlonjava/frontend/parser/StatementParser.java b/src/main/java/org/perlonjava/frontend/parser/StatementParser.java index 5a9632336..23072b059 100644 --- a/src/main/java/org/perlonjava/frontend/parser/StatementParser.java +++ b/src/main/java/org/perlonjava/frontend/parser/StatementParser.java @@ -531,11 +531,29 @@ public static Node parseWhenStatement(Parser parser) { BlockNode whenBlock = ParseBlock.parseBlock(parser); TokenUtils.consume(parser, LexerTokenType.OPERATOR, "}"); - // After a successful when match, Perl implicitly breaks out of the - // enclosing given block. Append `last;` to the when block so that - // execution leaves the surrounding bare-block (which acts as a - // single-iteration loop) once the matching block has run. - whenBlock.elements.add(new OperatorNode("last", new ListNode(index), index)); + // After a successful match, Perl returns the value of the when block + // and implicitly leaves the enclosing given block. Keep the final + // expression attached to our synthetic last so the backends can carry + // that value across the control-flow jump instead of compiling it in + // void context and replacing it with undef. + Node whenResult = null; + for (int i = whenBlock.elements.size() - 1; i >= 0; i--) { + Node element = whenBlock.elements.get(i); + if (element != null) { + whenResult = element; + whenBlock.elements.remove(i); + break; + } + } + if (whenResult == null) { + whenResult = new OperatorNode("undef", new ListNode(index), index); + } + OperatorNode implicitLast = new OperatorNode("last", new ListNode(index), index); + implicitLast.setAnnotation("implicitGivenLast", true); + // Store the value out-of-band so generic visitors never mistake it for + // a user-written dynamic label expression on `last EXPR`. + implicitLast.setAnnotation("implicitGivenResult", whenResult); + whenBlock.elements.add(implicitLast); // Determine whether to smart-match against $_ or use the value directly. // Per perlsyn, when(EXPR) skips the implicit `$_ ~~` and uses EXPR @@ -646,12 +664,13 @@ public static Node parseGivenStatement(Parser parser) { // Create the complete block: { $_ = EXPR; blockContent } List statements = new ArrayList<>(); - // $_ = condition (use proper $_ structure) + // local $_ = condition (given dynamically localizes the topic) Node dollarUnderscore = new OperatorNode("$", new IdentifierNode("_", index), index); + Node localTopic = new OperatorNode("local", dollarUnderscore, index); statements.add(new BinaryOperatorNode("=", - dollarUnderscore, + localTopic, condition, index)); diff --git a/src/main/java/org/perlonjava/frontend/parser/StringParser.java b/src/main/java/org/perlonjava/frontend/parser/StringParser.java index d72bffc53..526d6b579 100644 --- a/src/main/java/org/perlonjava/frontend/parser/StringParser.java +++ b/src/main/java/org/perlonjava/frontend/parser/StringParser.java @@ -19,6 +19,7 @@ import static org.perlonjava.runtime.perlmodule.Strict.HINT_RE_ASCII; import static org.perlonjava.runtime.perlmodule.Strict.HINT_RE_EVAL; import static org.perlonjava.runtime.perlmodule.Strict.HINT_RE_UNICODE; +import static org.perlonjava.runtime.perlmodule.Strict.HINT_RE_TAINT; import static org.perlonjava.runtime.perlmodule.Strict.HINT_LOCALE; import static org.perlonjava.runtime.runtimetypes.NameNormalizer.normalizeVariableName; import static org.perlonjava.runtime.runtimetypes.ScalarUtils.printable; @@ -520,6 +521,11 @@ public static OperatorNode parseRegexReplace(EmitterContext ctx, ParsedString ra String operator = "replaceRegex"; String replaceStr = rawStr.buffers.get(1); String modifierStr = rawStr.buffers.get(2); + if (ctx.symbolTable != null + && ctx.symbolTable.isStrictOptionEnabled(HINT_RE_TAINT) + && !modifierStr.contains("T")) { + modifierStr = "T" + modifierStr; + } Node parsed = parseRegexString(ctx, rawStr, parser, modifierStr); Node replace; @@ -593,6 +599,9 @@ public static OperatorNode parseRegexMatch(EmitterContext ctx, String operator, if (ctx.symbolTable.isStrictOptionEnabled(HINT_RE_EVAL) && !modStr.contains("E")) { modStr = "E" + modStr; } + if (ctx.symbolTable.isStrictOptionEnabled(HINT_RE_TAINT) && !modStr.contains("T")) { + modStr = "T" + modStr; + } } Node parsed = parseRegexString(ctx, rawStr, parser, modStr, isQuoteRegex); diff --git a/src/main/java/org/perlonjava/runtime/nativ/ExtendedNativeUtils.java b/src/main/java/org/perlonjava/runtime/nativ/ExtendedNativeUtils.java index 0e32da315..64a4e2009 100644 --- a/src/main/java/org/perlonjava/runtime/nativ/ExtendedNativeUtils.java +++ b/src/main/java/org/perlonjava/runtime/nativ/ExtendedNativeUtils.java @@ -55,28 +55,28 @@ private static RuntimeList passwdToList(FFMPosixInterface.PasswdEntry pw) { String shell = pw.shell(); long expire = pw.expire(); RuntimeArray.push(result, new RuntimeScalar(name)); - RuntimeArray.push(result, new RuntimeScalar(passwd)); + RuntimeArray.push(result, new RuntimeScalar(passwd).taintFromExternalInput()); RuntimeArray.push(result, new RuntimeScalar(uid)); RuntimeArray.push(result, new RuntimeScalar(gid)); RuntimeArray.push(result, new RuntimeScalar(change)); RuntimeArray.push(result, new RuntimeScalar("")); - RuntimeArray.push(result, new RuntimeScalar(gecos)); + RuntimeArray.push(result, new RuntimeScalar(gecos).taintFromExternalInput()); RuntimeArray.push(result, new RuntimeScalar(dir)); - RuntimeArray.push(result, new RuntimeScalar(shell)); + RuntimeArray.push(result, new RuntimeScalar(shell).taintFromExternalInput()); RuntimeArray.push(result, new RuntimeScalar(expire)); } else { String gecos = pw.gecos(); String dir = pw.dir(); String shell = pw.shell(); RuntimeArray.push(result, new RuntimeScalar(name)); - RuntimeArray.push(result, new RuntimeScalar(passwd)); + RuntimeArray.push(result, new RuntimeScalar(passwd).taintFromExternalInput()); RuntimeArray.push(result, new RuntimeScalar(uid)); RuntimeArray.push(result, new RuntimeScalar(gid)); RuntimeArray.push(result, new RuntimeScalar("")); RuntimeArray.push(result, new RuntimeScalar("")); - RuntimeArray.push(result, new RuntimeScalar(gecos)); + RuntimeArray.push(result, new RuntimeScalar(gecos).taintFromExternalInput()); RuntimeArray.push(result, new RuntimeScalar(dir)); - RuntimeArray.push(result, new RuntimeScalar(shell)); + RuntimeArray.push(result, new RuntimeScalar(shell).taintFromExternalInput()); RuntimeArray.push(result, new RuntimeScalar("")); } return result.getList(); diff --git a/src/main/java/org/perlonjava/runtime/nativ/NativeUtils.java b/src/main/java/org/perlonjava/runtime/nativ/NativeUtils.java index a4f3f4fc7..e333e37dd 100644 --- a/src/main/java/org/perlonjava/runtime/nativ/NativeUtils.java +++ b/src/main/java/org/perlonjava/runtime/nativ/NativeUtils.java @@ -25,6 +25,8 @@ public static RuntimeScalar symlink(int ctx, RuntimeBase... args) { return new RuntimeScalar(0); } + RuntimeScalar.checkTaint(args[0].scalar(), "symlink"); + RuntimeScalar.checkTaint(args[1].scalar(), "symlink"); String oldFile = RuntimeIO.sanitizePathname("symlink", args[0].toString()); Path link = RuntimeIO.resolvePath(args[1].toString(), "symlink"); @@ -72,6 +74,8 @@ public static RuntimeScalar link(int ctx, RuntimeBase... args) { return new RuntimeScalar(0); } + RuntimeScalar.checkTaint(args[0].scalar(), "link"); + RuntimeScalar.checkTaint(args[1].scalar(), "link"); String oldFile = RuntimeIO.resolvePath(args[0].toString()).toString(); String newFile = RuntimeIO.resolvePath(args[1].toString()).toString(); diff --git a/src/main/java/org/perlonjava/runtime/operators/BitwiseOperators.java b/src/main/java/org/perlonjava/runtime/operators/BitwiseOperators.java index b54cb195c..1590bc9b2 100644 --- a/src/main/java/org/perlonjava/runtime/operators/BitwiseOperators.java +++ b/src/main/java/org/perlonjava/runtime/operators/BitwiseOperators.java @@ -28,7 +28,7 @@ public static RuntimeScalar bitwiseAnd(RuntimeScalar runtimeScalar, RuntimeScala int t2 = arg2.type; if (t1 == RuntimeScalarType.INTEGER && t2 == RuntimeScalarType.INTEGER) { int result = ((int) runtimeScalar.value) & ((int) arg2.value); - return new RuntimeScalar(Integer.toUnsignedLong(result)); + return new RuntimeScalar(Integer.toUnsignedLong(result)).propagateTaint(runtimeScalar, arg2); } // Check for overloaded '&' operator on blessed objects @@ -36,7 +36,7 @@ public static RuntimeScalar bitwiseAnd(RuntimeScalar runtimeScalar, RuntimeScala int blessId2 = blessedId(arg2); if (blessId < 0 || blessId2 < 0) { RuntimeScalar result = OverloadContext.tryTwoArgumentOverload(runtimeScalar, arg2, blessId, blessId2, "(&", "&"); - if (result != null) return result; + if (result != null) return result.propagateTaint(runtimeScalar, arg2); } // Fetch tied/readonly scalars once to avoid redundant FETCH calls @@ -81,7 +81,7 @@ public static RuntimeScalar bitwiseAndBinary(RuntimeScalar runtimeScalar, Runtim long val2 = arg2.getLong() & 0xFFFFFFFFL; long result = (val1 & val2) & 0xFFFFFFFFL; - return new RuntimeScalar(result); + return new RuntimeScalar(result).propagateTaint(runtimeScalar, arg2); } /** @@ -99,7 +99,7 @@ public static RuntimeScalar bitwiseOr(RuntimeScalar runtimeScalar, RuntimeScalar int t2 = arg2.type; if (t1 == RuntimeScalarType.INTEGER && t2 == RuntimeScalarType.INTEGER) { int result = ((int) runtimeScalar.value) | ((int) arg2.value); - return new RuntimeScalar(Integer.toUnsignedLong(result)); + return new RuntimeScalar(Integer.toUnsignedLong(result)).propagateTaint(runtimeScalar, arg2); } // Check for overloaded '|' operator on blessed objects @@ -107,7 +107,7 @@ public static RuntimeScalar bitwiseOr(RuntimeScalar runtimeScalar, RuntimeScalar int blessId2 = blessedId(arg2); if (blessId < 0 || blessId2 < 0) { RuntimeScalar result = OverloadContext.tryTwoArgumentOverload(runtimeScalar, arg2, blessId, blessId2, "(|", "|"); - if (result != null) return result; + if (result != null) return result.propagateTaint(runtimeScalar, arg2); } // Fetch tied/readonly scalars once to avoid redundant FETCH calls @@ -142,7 +142,7 @@ public static RuntimeScalar bitwiseOrBinary(RuntimeScalar runtimeScalar, Runtime long val2 = arg2.getLong() & 0xFFFFFFFFL; long result = (val1 | val2) & 0xFFFFFFFFL; - return new RuntimeScalar(result); + return new RuntimeScalar(result).propagateTaint(runtimeScalar, arg2); } /** @@ -164,7 +164,7 @@ public static RuntimeScalar bitwiseXor(RuntimeScalar runtimeScalar, RuntimeScala int t2 = arg2.type; if (t1 == RuntimeScalarType.INTEGER && t2 == RuntimeScalarType.INTEGER) { int result = ((int) runtimeScalar.value) ^ ((int) arg2.value); - return new RuntimeScalar(Integer.toUnsignedLong(result)); + return new RuntimeScalar(Integer.toUnsignedLong(result)).propagateTaint(runtimeScalar, arg2); } // Check for overloaded '^' operator on blessed objects @@ -172,7 +172,7 @@ public static RuntimeScalar bitwiseXor(RuntimeScalar runtimeScalar, RuntimeScala int blessId2 = blessedId(arg2); if (blessId < 0 || blessId2 < 0) { RuntimeScalar result = OverloadContext.tryTwoArgumentOverload(runtimeScalar, arg2, blessId, blessId2, "(^", "^"); - if (result != null) return result; + if (result != null) return result.propagateTaint(runtimeScalar, arg2); } // Fetch tied/readonly scalars once to avoid redundant FETCH calls @@ -207,7 +207,7 @@ public static RuntimeScalar bitwiseXorBinary(RuntimeScalar runtimeScalar, Runtim long val2 = arg2.getLong() & 0xFFFFFFFFL; long result = (val1 ^ val2) & 0xFFFFFFFFL; - return new RuntimeScalar(result); + return new RuntimeScalar(result).propagateTaint(runtimeScalar, arg2); } /** Numeric bitwise operations under {@code use integer}: return a signed IV. */ @@ -230,7 +230,7 @@ private static RuntimeScalar integerBitwiseBinary(RuntimeScalar arg1, RuntimeSca String symbol = Character.toString(operator); RuntimeScalar overloaded = OverloadContext.tryTwoArgumentOverload( arg1, arg2, blessId, blessId2, "(" + symbol, symbol); - if (overloaded != null) return overloaded; + if (overloaded != null) return overloaded.propagateTaint(arg1, arg2); } int a = nativeIntValue(arg1); int b = nativeIntValue(arg2); @@ -240,7 +240,7 @@ private static RuntimeScalar integerBitwiseBinary(RuntimeScalar arg1, RuntimeSca case '^' -> a ^ b; default -> throw new IllegalArgumentException("unknown bitwise operator: " + operator); }; - return new RuntimeScalar(result); + return new RuntimeScalar(result).propagateTaint(arg1, arg2); } private static int nativeIntValue(RuntimeScalar value) { @@ -266,7 +266,7 @@ public static RuntimeScalar bitwiseNot(RuntimeScalar runtimeScalar) { if (blessId < 0) { RuntimeScalar result = OverloadContext.tryOneArgumentOverload( runtimeScalar, blessId, "(~", "~", BitwiseOperators::bitwiseNot); - if (result != null) return result; + if (result != null) return result.propagateTaint(runtimeScalar); } // Fetch tied/readonly scalar once to avoid redundant FETCH calls @@ -279,9 +279,9 @@ public static RuntimeScalar bitwiseNot(RuntimeScalar runtimeScalar) { // - If it's a string, use string NOT (character-by-character) int vt = val.type; if (vt == RuntimeScalarType.INTEGER || vt == RuntimeScalarType.DOUBLE) { - return bitwiseNotBinary(val); + return bitwiseNotBinary(val).propagateTaint(runtimeScalar); } - return bitwiseNotDot(val); + return bitwiseNotDot(val).propagateTaint(runtimeScalar); } /** @@ -303,7 +303,7 @@ public static RuntimeScalar bitwiseNotBinary(RuntimeScalar runtimeScalar) { // Apply bitwise NOT and mask to 32 bits long result = (~masked32bit) & 0xFFFFFFFFL; - return new RuntimeScalar(result); + return new RuntimeScalar(result).propagateTaint(runtimeScalar); } /** @@ -319,7 +319,7 @@ public static RuntimeScalar integerBitwiseNot(RuntimeScalar runtimeScalar) { if (blessId < 0) { RuntimeScalar result = OverloadContext.tryOneArgumentOverload( runtimeScalar, blessId, "(~", "~", BitwiseOperators::integerBitwiseNot); - if (result != null) return result; + if (result != null) return result.propagateTaint(runtimeScalar); } // Fetch tied/readonly scalar once to avoid redundant FETCH calls @@ -332,7 +332,7 @@ public static RuntimeScalar integerBitwiseNot(RuntimeScalar runtimeScalar) { // - If it's a string, use string NOT (character-by-character) int vt = val.type; if (vt != RuntimeScalarType.INTEGER && vt != RuntimeScalarType.DOUBLE) { - return bitwiseNotDot(val); + return bitwiseNotDot(val).propagateTaint(runtimeScalar); } // Must use 32-bit int (not long) to match ivsize=4 in Config.pm. @@ -341,7 +341,7 @@ public static RuntimeScalar integerBitwiseNot(RuntimeScalar runtimeScalar) { int value = (int) val.getLong(); int result = ~value; - return new RuntimeScalar(result); + return new RuntimeScalar(result).propagateTaint(runtimeScalar); } /** @@ -369,7 +369,8 @@ public static RuntimeScalar bitwiseAndDot(RuntimeScalar runtimeScalar, RuntimeSc return stringBitwiseResult(result.toString(), runtimeScalar.type == RuntimeScalarType.STRING - || arg2.type == RuntimeScalarType.STRING); + || arg2.type == RuntimeScalarType.STRING) + .propagateTaint(runtimeScalar, arg2); } /** @@ -397,7 +398,8 @@ public static RuntimeScalar bitwiseOrDot(RuntimeScalar runtimeScalar, RuntimeSca return stringBitwiseResult(result.toString(), runtimeScalar.type == RuntimeScalarType.STRING - || arg2.type == RuntimeScalarType.STRING); + || arg2.type == RuntimeScalarType.STRING) + .propagateTaint(runtimeScalar, arg2); } /** @@ -425,7 +427,8 @@ public static RuntimeScalar bitwiseXorDot(RuntimeScalar runtimeScalar, RuntimeSc return stringBitwiseResult(result.toString(), runtimeScalar.type == RuntimeScalarType.STRING - || arg2.type == RuntimeScalarType.STRING); + || arg2.type == RuntimeScalarType.STRING) + .propagateTaint(runtimeScalar, arg2); } /** @@ -450,7 +453,7 @@ public static RuntimeScalar bitwiseNotDot(RuntimeScalar runtimeScalar) { // Perl's string complement returns an octet string even when the // operand carries the UTF-8 flag (for code points representable as // bytes, which is the range accepted above). - return stringBitwiseResult(result.toString(), false); + return stringBitwiseResult(result.toString(), false).propagateTaint(runtimeScalar); } private static RuntimeScalar stringBitwiseResult(String value, boolean utf8) { diff --git a/src/main/java/org/perlonjava/runtime/operators/ChownOperator.java b/src/main/java/org/perlonjava/runtime/operators/ChownOperator.java index 2c0bc530c..6d5ec98ad 100644 --- a/src/main/java/org/perlonjava/runtime/operators/ChownOperator.java +++ b/src/main/java/org/perlonjava/runtime/operators/ChownOperator.java @@ -21,6 +21,12 @@ public class ChownOperator { * @return RuntimeScalar with count of successfully changed files */ public static RuntimeScalar chown(int ctx, RuntimeBase... args) { + for (RuntimeBase arg : args) { + for (RuntimeScalar scalar : arg) { + RuntimeScalar.checkTaint(scalar, "chown"); + } + } + if (args.length < 2) { // Need at least uid and gid return new RuntimeScalar(0); @@ -262,4 +268,4 @@ public static boolean isChownRestricted() { // This is a safe default assumption return true; } -} \ No newline at end of file +} diff --git a/src/main/java/org/perlonjava/runtime/operators/CompareOperators.java b/src/main/java/org/perlonjava/runtime/operators/CompareOperators.java index ebdc9aa20..fd0f50fcf 100644 --- a/src/main/java/org/perlonjava/runtime/operators/CompareOperators.java +++ b/src/main/java/org/perlonjava/runtime/operators/CompareOperators.java @@ -706,6 +706,20 @@ public static RuntimeScalar smartmatch(RuntimeScalar arg1, RuntimeScalar arg2) { if (result != null) return result; } + // Scalar ~~ ARRAY matches when the scalar smartmatches any array + // element. Keep the original scalar intact across candidates: tainted + // strings must not have their backing value consumed by a failed + // comparison before a later element matches. + if (arg2.type == RuntimeScalarType.ARRAYREFERENCE + && arg2.value instanceof RuntimeArray candidates) { + for (RuntimeScalar candidate : candidates) { + if (smartmatch(arg1, candidate).getBoolean()) { + return scalarTrue; + } + } + return scalarFalse; + } + // Check if both are defined if (!arg1.getDefinedBoolean() && !arg2.getDefinedBoolean()) { return scalarTrue; // undef ~~ undef is true diff --git a/src/main/java/org/perlonjava/runtime/operators/Crypt.java b/src/main/java/org/perlonjava/runtime/operators/Crypt.java index 1aa191944..f3891497d 100644 --- a/src/main/java/org/perlonjava/runtime/operators/Crypt.java +++ b/src/main/java/org/perlonjava/runtime/operators/Crypt.java @@ -50,7 +50,7 @@ public static RuntimeScalar crypt(RuntimeList args) { } String hashed = hashWithSalt(plaintext, salt); - return new RuntimeScalar(hashed); + return new RuntimeScalar(hashed).propagateTaint(plaintextScalar, saltScalar); } /** diff --git a/src/main/java/org/perlonjava/runtime/operators/Directory.java b/src/main/java/org/perlonjava/runtime/operators/Directory.java index 92d828966..fe94c4e3d 100644 --- a/src/main/java/org/perlonjava/runtime/operators/Directory.java +++ b/src/main/java/org/perlonjava/runtime/operators/Directory.java @@ -22,6 +22,7 @@ public class Directory { public static RuntimeScalar chdir(RuntimeScalar runtimeScalar) { + RuntimeScalar.checkTaint(runtimeScalar, "chdir"); // chdir EXPR // chdir FILEHANDLE // chdir DIRHANDLE @@ -109,6 +110,7 @@ public static RuntimeScalar chdir(RuntimeScalar runtimeScalar) { } public static RuntimeScalar rmdir(RuntimeScalar runtimeScalar) { + RuntimeScalar.checkTaint(runtimeScalar, "rmdir"); String dirName = runtimeScalar.toString(); try { @@ -238,6 +240,11 @@ public static RuntimeScalar seekdir(RuntimeList args) { } public static RuntimeScalar mkdir(RuntimeList args) { + if (!args.elements.isEmpty()) { + RuntimeScalar.checkTaint(args.elements.getFirst().scalar(), "mkdir"); + } else { + RuntimeScalar.checkTaint(getGlobalVariable("main::_"), "mkdir"); + } String fileName; int mode; diff --git a/src/main/java/org/perlonjava/runtime/operators/IOOperator.java b/src/main/java/org/perlonjava/runtime/operators/IOOperator.java index a5918df28..d794ee3e2 100644 --- a/src/main/java/org/perlonjava/runtime/operators/IOOperator.java +++ b/src/main/java/org/perlonjava/runtime/operators/IOOperator.java @@ -693,6 +693,9 @@ public static RuntimeScalar open(int ctx, RuntimeBase... args) { RuntimeIO fh; if (mode.contains("|")) { + for (int i = 1; i < args.length; i++) { + RuntimeScalar.checkTaint(args[i].scalar(), "piped open"); + } // Check for fork-open pattern: open FH, "-|" or open FH, "|-" with no command // This is the 2-arg piped open that normally forks in Perl if (args.length == 2 && (mode.equals("-|") || mode.equals("|-"))) { @@ -710,6 +713,12 @@ public static RuntimeScalar open(int ctx, RuntimeBase... args) { } else if (args.length > 2) { // 3-argument open RuntimeScalar secondArg = args[2].scalar(); + boolean canWrite = mode.contains(">") || mode.startsWith("+"); + + if (canWrite) { + RuntimeScalar.checkTaint(args[1].scalar(), "open"); + RuntimeScalar.checkTaint(secondArg, "open"); + } // Check for filehandle duplication modes (<&, >&, >>&, +<&, +>&, +>>& and &= variants) if (mode.equals("<&") || mode.equals(">&") || mode.equals(">>&") || @@ -832,6 +841,9 @@ else if (secondArg.type == RuntimeScalarType.GLOB || secondArg.type == RuntimeSc } } else { // 2-argument open + if (mode.startsWith(">") || mode.startsWith("+") || mode.startsWith("|")) { + RuntimeScalar.checkTaint(args[1].scalar(), "open"); + } fh = RuntimeIO.open(mode); } if (fh == null) { @@ -951,6 +963,7 @@ public static RuntimeScalar printf(RuntimeList runtimeList, RuntimeScalar fileHa } RuntimeScalar format = (RuntimeScalar) flatList.elements.removeFirst(); // Extract the format string from elements + RuntimeScalar.checkTaint(format, "printf"); String formattedString; @@ -1481,8 +1494,10 @@ public static RuntimeScalar sysopen(int ctx, RuntimeBase... args) { } RuntimeScalar fileHandle = args[0].scalar(); - String fileName = args[1].toString(); - int mode = args[2].scalar().getInt(); + RuntimeScalar fileNameArg = args[1].scalar(); + RuntimeScalar modeArg = args[2].scalar(); + String fileName = fileNameArg.toString(); + int mode = modeArg.getInt(); int perms = 0666; // Default permissions (octal) if (args.length >= 4) { @@ -1502,15 +1517,24 @@ public static RuntimeScalar sysopen(int ctx, RuntimeBase... args) { int O_TRUNC = 01000; // 512 in decimal int O_NOFOLLOW = 0400000; // Reject a symlink in the final path component + int baseMode = mode & 3; // Get the lowest 2 bits + boolean canWrite = baseMode != O_RDONLY + || (mode & (O_CREAT | O_APPEND | O_TRUNC)) != 0; + if (canWrite) { + RuntimeScalar.checkTaint(fileNameArg, "sysopen"); + RuntimeScalar.checkTaint(modeArg, "sysopen"); + if (args.length >= 4) { + RuntimeScalar.checkTaint(args[3].scalar(), "sysopen"); + } + } + File file = RuntimeIO.resolveFile(fileName); if ((mode & O_NOFOLLOW) != 0 && Files.isSymbolicLink(file.toPath())) { getGlobalVariable("main::!").set("Too many levels of symbolic links"); - return scalarFalse; + return scalarUndef; } // Determine the base mode - int baseMode = mode & 3; // Get the lowest 2 bits - if (baseMode == O_RDONLY) { modeStr = "<"; } else if (baseMode == O_WRONLY) { @@ -1535,7 +1559,7 @@ public static RuntimeScalar sysopen(int ctx, RuntimeBase... args) { // O_EXCL: "error if O_CREAT and the file already exists" if ((mode & O_EXCL) != 0 && existed) { getGlobalVariable("main::!").set("File exists"); - return scalarFalse; + return scalarUndef; } if (!existed) { try { @@ -1545,14 +1569,14 @@ public static RuntimeScalar sysopen(int ctx, RuntimeBase... args) { } catch (IOException e) { // Failed to create file getGlobalVariable("main::!").set(e.getMessage()); - return scalarFalse; + return scalarUndef; } } } RuntimeIO fh = RuntimeIO.open(fileName, modeStr); if (fh == null) { - return scalarFalse; + return scalarUndef; } // Set IO slot on the glob, following the same pattern as open() and socket() @@ -1783,15 +1807,19 @@ public static RuntimeScalar formline(int ctx, RuntimeBase... args) { } // Get the format template - String formatTemplate = args[0].scalar().toString(); + RuntimeScalar picture = args[0].scalar(); + String formatTemplate = picture.toString(); // For simple cases (like constants in index.t), if there are no format fields, // just append the template string directly to $^A if (!formatTemplate.contains("@") && !formatTemplate.contains("^")) { // Simple case: no format fields, just append the string RuntimeScalar accumulator = getGlobalVariable(GlobalContext.encodeSpecialVar("A")); + boolean resultTainted = accumulator.isTainted() || picture.isTainted() + || picture.formatPictureTainted; String currentValue = accumulator.toString(); accumulator.set(currentValue + formatTemplate); + accumulator.tainted = resultTainted; return scalarTrue; } @@ -1818,8 +1846,14 @@ public static RuntimeScalar formline(int ctx, RuntimeBase... args) { // Append to $^A RuntimeScalar accumulator = getGlobalVariable(GlobalContext.encodeSpecialVar("A")); + boolean resultTainted = accumulator.isTainted() || picture.isTainted() + || picture.formatPictureTainted; + for (int i = 1; i < args.length; i++) { + resultTainted |= args[i].scalar().isTainted(); + } String currentValue = accumulator.toString(); accumulator.set(currentValue + formattedOutput); + accumulator.tainted = resultTainted; // Return success (1) return scalarTrue; @@ -2281,6 +2315,9 @@ public static RuntimeScalar truncate(int ctx, RuntimeBase... args) { return scalarFalse; } + RuntimeScalar.checkTaint(args[0].scalar(), "truncate"); + RuntimeScalar.checkTaint(args[1].scalar(), "truncate"); + try { RuntimeBase firstArg = args[0]; long length = args[1].scalar().getLong(); @@ -2370,6 +2407,9 @@ public static RuntimeScalar fcntl(int ctx, RuntimeBase... args) { return scalarFalse; } + RuntimeScalar.checkTaint(args[1].scalar(), "fcntl"); + RuntimeScalar.checkTaint(args[2].scalar(), "fcntl"); + try { RuntimeScalar fileHandle = args[0].scalar(); int function = args[1].scalar().getInt(); @@ -2443,6 +2483,9 @@ public static RuntimeScalar ioctl(int ctx, RuntimeBase... args) { return scalarFalse; } + RuntimeScalar.checkTaint(args[1].scalar(), "ioctl"); + RuntimeScalar.checkTaint(args[2].scalar(), "ioctl"); + try { RuntimeScalar fileHandle = args[0].scalar(); long request = args[1].scalar().getLong(); diff --git a/src/main/java/org/perlonjava/runtime/operators/KillOperator.java b/src/main/java/org/perlonjava/runtime/operators/KillOperator.java index 10ae32f48..5c5ce44b5 100644 --- a/src/main/java/org/perlonjava/runtime/operators/KillOperator.java +++ b/src/main/java/org/perlonjava/runtime/operators/KillOperator.java @@ -3,6 +3,8 @@ import org.perlonjava.runtime.nativ.NativeUtils; import org.perlonjava.runtime.nativ.ffm.FFMPosix; import org.perlonjava.runtime.nativ.ffm.FFMPosixInterface; +import org.perlonjava.runtime.runtimetypes.GlobalContext; +import org.perlonjava.runtime.runtimetypes.PerlCompilerException; import org.perlonjava.runtime.runtimetypes.PerlSignalQueue; import org.perlonjava.runtime.runtimetypes.RuntimeBase; import org.perlonjava.runtime.runtimetypes.RuntimeScalar; @@ -25,12 +27,18 @@ public class KillOperator { * @return RuntimeScalar with count of successfully signaled processes */ public static RuntimeScalar kill(int ctx, RuntimeBase... args) { + boolean precedingJoinWasTainted = GlobalContext.consumeThreadJoinTaint(); if (args.length < 2) { + if (GlobalContext.isTaintModeActive() && precedingJoinWasTainted) { + throw new PerlCompilerException( + "Insecure dependency in kill while running with -T switch"); + } return new RuntimeScalar(0); } // First argument is the signal RuntimeScalar signalArg = args[0].getFirst(); + RuntimeScalar.checkTaint(signalArg, "kill"); int signal; // Handle named signals (e.g., "TERM", "KILL", "HUP") @@ -52,6 +60,7 @@ public static RuntimeScalar kill(int ctx, RuntimeBase... args) { // Process each PID starting from second argument for (int i = 1; i < args.length; i++) { for (RuntimeScalar scalar : args[i]) { + RuntimeScalar.checkTaint(scalar, "kill"); int pid = scalar.getInt(); // Special case: negative PID means process group diff --git a/src/main/java/org/perlonjava/runtime/operators/MathOperators.java b/src/main/java/org/perlonjava/runtime/operators/MathOperators.java index 9455434b1..3e6795e87 100644 --- a/src/main/java/org/perlonjava/runtime/operators/MathOperators.java +++ b/src/main/java/org/perlonjava/runtime/operators/MathOperators.java @@ -233,6 +233,10 @@ public static RuntimeScalar addWarn(RuntimeScalar arg1, int arg2) { * @return A new RuntimeScalar representing the sum. */ public static RuntimeScalar add(RuntimeScalar arg1, RuntimeScalar arg2) { + return addUnpropagated(arg1, arg2).propagateTaint(arg1, arg2); + } + + private static RuntimeScalar addUnpropagated(RuntimeScalar arg1, RuntimeScalar arg2) { // Fast path: both INTEGER - skip blessedId check, getNumber(), type checks if (arg1.type == INTEGER && arg2.type == INTEGER) { int a = (int) arg1.value; @@ -279,6 +283,10 @@ public static RuntimeScalar add(RuntimeScalar arg1, RuntimeScalar arg2) { * @return A new RuntimeScalar representing the sum. */ public static RuntimeScalar addWarn(RuntimeScalar arg1, RuntimeScalar arg2) { + return addWarnUnpropagated(arg1, arg2).propagateTaint(arg1, arg2); + } + + private static RuntimeScalar addWarnUnpropagated(RuntimeScalar arg1, RuntimeScalar arg2) { // Fast path: both INTEGER - skip blessedId check, getNumber(), type checks if (arg1.type == INTEGER && arg2.type == INTEGER) { int a = (int) arg1.value; @@ -389,6 +397,10 @@ public static RuntimeScalar subtractWarn(RuntimeScalar arg1, int arg2) { * @return A new RuntimeScalar representing the difference. */ public static RuntimeScalar subtract(RuntimeScalar arg1, RuntimeScalar arg2) { + return subtractUnpropagated(arg1, arg2).propagateTaint(arg1, arg2); + } + + private static RuntimeScalar subtractUnpropagated(RuntimeScalar arg1, RuntimeScalar arg2) { // Fast path: both INTEGER - skip blessedId check, getNumber(), type checks if (arg1.type == INTEGER && arg2.type == INTEGER) { int a = (int) arg1.value; @@ -435,6 +447,10 @@ public static RuntimeScalar subtract(RuntimeScalar arg1, RuntimeScalar arg2) { * @return A new RuntimeScalar representing the difference. */ public static RuntimeScalar subtractWarn(RuntimeScalar arg1, RuntimeScalar arg2) { + return subtractWarnUnpropagated(arg1, arg2).propagateTaint(arg1, arg2); + } + + private static RuntimeScalar subtractWarnUnpropagated(RuntimeScalar arg1, RuntimeScalar arg2) { // Fast path: both INTEGER - skip blessedId check, getNumber(), type checks if (arg1.type == INTEGER && arg2.type == INTEGER) { int a = (int) arg1.value; @@ -482,6 +498,10 @@ public static RuntimeScalar subtractWarn(RuntimeScalar arg1, RuntimeScalar arg2) * @return A new RuntimeScalar representing the product. */ public static RuntimeScalar multiply(RuntimeScalar arg1, RuntimeScalar arg2) { + return multiplyUnpropagated(arg1, arg2).propagateTaint(arg1, arg2); + } + + private static RuntimeScalar multiplyUnpropagated(RuntimeScalar arg1, RuntimeScalar arg2) { // Fast path: both INTEGER - skip blessedId check, getNumber(), type checks if (arg1.type == INTEGER && arg2.type == INTEGER) { int a = (int) arg1.value; @@ -529,6 +549,10 @@ public static RuntimeScalar multiply(RuntimeScalar arg1, RuntimeScalar arg2) { * @return A new RuntimeScalar representing the product. */ public static RuntimeScalar multiplyWarn(RuntimeScalar arg1, RuntimeScalar arg2) { + return multiplyWarnUnpropagated(arg1, arg2).propagateTaint(arg1, arg2); + } + + private static RuntimeScalar multiplyWarnUnpropagated(RuntimeScalar arg1, RuntimeScalar arg2) { // Fast path: both INTEGER - skip blessedId check, getNumber(), type checks if (arg1.type == INTEGER && arg2.type == INTEGER) { int a = (int) arg1.value; @@ -577,6 +601,10 @@ public static RuntimeScalar multiplyWarn(RuntimeScalar arg1, RuntimeScalar arg2) * @throws PerlCompilerException if division by zero occurs. */ public static RuntimeScalar divide(RuntimeScalar arg1, RuntimeScalar arg2) { + return divideUnpropagated(arg1, arg2).propagateTaint(arg1, arg2); + } + + private static RuntimeScalar divideUnpropagated(RuntimeScalar arg1, RuntimeScalar arg2) { // Prepare overload context and check if object is eligible for overloading int blessId = blessedId(arg1); int blessId2 = blessedId(arg2); @@ -614,6 +642,10 @@ public static RuntimeScalar divide(RuntimeScalar arg1, RuntimeScalar arg2) { * @throws PerlCompilerException if division by zero occurs. */ public static RuntimeScalar divideWarn(RuntimeScalar arg1, RuntimeScalar arg2) { + return divideWarnUnpropagated(arg1, arg2).propagateTaint(arg1, arg2); + } + + private static RuntimeScalar divideWarnUnpropagated(RuntimeScalar arg1, RuntimeScalar arg2) { // Prepare overload context and check if object is eligible for overloading int blessId = blessedId(arg1); int blessId2 = blessedId(arg2); @@ -650,6 +682,10 @@ public static RuntimeScalar divideWarn(RuntimeScalar arg1, RuntimeScalar arg2) { * @return A new RuntimeScalar representing the modulus. */ public static RuntimeScalar modulus(RuntimeScalar arg1, RuntimeScalar arg2) { + return modulusUnpropagated(arg1, arg2).propagateTaint(arg1, arg2); + } + + private static RuntimeScalar modulusUnpropagated(RuntimeScalar arg1, RuntimeScalar arg2) { // Prepare overload context and check if object is eligible for overloading int blessId = blessedId(arg1); int blessId2 = blessedId(arg2); @@ -689,6 +725,10 @@ public static RuntimeScalar modulus(RuntimeScalar arg1, RuntimeScalar arg2) { * @return A new RuntimeScalar representing the modulus. */ public static RuntimeScalar modulusWarn(RuntimeScalar arg1, RuntimeScalar arg2) { + return modulusWarnUnpropagated(arg1, arg2).propagateTaint(arg1, arg2); + } + + private static RuntimeScalar modulusWarnUnpropagated(RuntimeScalar arg1, RuntimeScalar arg2) { // Prepare overload context and check if object is eligible for overloading int blessId = blessedId(arg1); int blessId2 = blessedId(arg2); @@ -1105,6 +1145,10 @@ private static double truncate(double value) { * @return A new RuntimeScalar representing the natural logarithm. */ public static RuntimeScalar log(RuntimeScalar runtimeScalar) { + return logUnpropagated(runtimeScalar).propagateTaint(runtimeScalar); + } + + private static RuntimeScalar logUnpropagated(RuntimeScalar runtimeScalar) { // Check if object is eligible for overloading int blessId = blessedId(runtimeScalar); if (blessId < 0) { @@ -1126,6 +1170,10 @@ public static RuntimeScalar log(RuntimeScalar runtimeScalar) { * @return A new RuntimeScalar representing the square root. */ public static RuntimeScalar sqrt(RuntimeScalar runtimeScalar) { + return sqrtUnpropagated(runtimeScalar).propagateTaint(runtimeScalar); + } + + private static RuntimeScalar sqrtUnpropagated(RuntimeScalar runtimeScalar) { // Check if object is eligible for overloading int blessId = blessedId(runtimeScalar); if (blessId < 0) { @@ -1147,6 +1195,10 @@ public static RuntimeScalar sqrt(RuntimeScalar runtimeScalar) { * @return A new RuntimeScalar representing the cosine. */ public static RuntimeScalar cos(RuntimeScalar runtimeScalar) { + return cosUnpropagated(runtimeScalar).propagateTaint(runtimeScalar); + } + + private static RuntimeScalar cosUnpropagated(RuntimeScalar runtimeScalar) { // Check if object is eligible for overloading int blessId = blessedId(runtimeScalar); if (blessId < 0) { @@ -1164,6 +1216,10 @@ public static RuntimeScalar cos(RuntimeScalar runtimeScalar) { * @return A new RuntimeScalar representing the sine. */ public static RuntimeScalar sin(RuntimeScalar runtimeScalar) { + return sinUnpropagated(runtimeScalar).propagateTaint(runtimeScalar); + } + + private static RuntimeScalar sinUnpropagated(RuntimeScalar runtimeScalar) { // Check if object is eligible for overloading int blessId = blessedId(runtimeScalar); if (blessId < 0) { @@ -1181,6 +1237,10 @@ public static RuntimeScalar sin(RuntimeScalar runtimeScalar) { * @return A new RuntimeScalar representing the exponential. */ public static RuntimeScalar exp(RuntimeScalar runtimeScalar) { + return expUnpropagated(runtimeScalar).propagateTaint(runtimeScalar); + } + + private static RuntimeScalar expUnpropagated(RuntimeScalar runtimeScalar) { // Check if object is eligible for overloading int blessId = blessedId(runtimeScalar); if (blessId < 0) { @@ -1200,6 +1260,10 @@ public static RuntimeScalar exp(RuntimeScalar runtimeScalar) { * @return A new RuntimeScalar representing the power. */ public static RuntimeScalar pow(RuntimeScalar arg1, RuntimeScalar arg2) { + return powUnpropagated(arg1, arg2).propagateTaint(arg1, arg2); + } + + private static RuntimeScalar powUnpropagated(RuntimeScalar arg1, RuntimeScalar arg2) { // Prepare overload context and check if object is eligible for overloading int blessId = blessedId(arg1); int blessId2 = blessedId(arg2); @@ -1220,6 +1284,10 @@ public static RuntimeScalar pow(RuntimeScalar arg1, RuntimeScalar arg2) { * @return A new RuntimeScalar representing the power. */ public static RuntimeScalar powWarn(RuntimeScalar arg1, RuntimeScalar arg2) { + return powWarnUnpropagated(arg1, arg2).propagateTaint(arg1, arg2); + } + + private static RuntimeScalar powWarnUnpropagated(RuntimeScalar arg1, RuntimeScalar arg2) { // Prepare overload context and check if object is eligible for overloading int blessId = blessedId(arg1); int blessId2 = blessedId(arg2); @@ -1246,6 +1314,10 @@ public static RuntimeScalar powWarn(RuntimeScalar arg1, RuntimeScalar arg2) { * @return A new RuntimeScalar representing the angle theta in radians. */ public static RuntimeScalar atan2(RuntimeScalar arg1, RuntimeScalar arg2) { + return atan2Unpropagated(arg1, arg2).propagateTaint(arg1, arg2); + } + + private static RuntimeScalar atan2Unpropagated(RuntimeScalar arg1, RuntimeScalar arg2) { // Prepare overload context and check if object is eligible for overloading int blessId = blessedId(arg1); int blessId2 = blessedId(arg2); @@ -1264,6 +1336,10 @@ public static RuntimeScalar atan2(RuntimeScalar arg1, RuntimeScalar arg2) { * @return A new RuntimeScalar representing the absolute value. */ public static RuntimeScalar abs(RuntimeScalar runtimeScalar) { + return absUnpropagated(runtimeScalar).propagateTaint(runtimeScalar); + } + + private static RuntimeScalar absUnpropagated(RuntimeScalar runtimeScalar) { // Check if object is eligible for overloading int blessId = blessedId(runtimeScalar); if (blessId < 0) { @@ -1292,6 +1368,10 @@ public static RuntimeScalar abs(RuntimeScalar runtimeScalar) { * Fast path - no warning checks. */ public static RuntimeScalar unaryMinus(RuntimeScalar runtimeScalar) { + return unaryMinusUnpropagated(runtimeScalar).propagateTaint(runtimeScalar); + } + + private static RuntimeScalar unaryMinusUnpropagated(RuntimeScalar runtimeScalar) { // Check if object is eligible for overloading int blessId = blessedId(runtimeScalar); if (blessId < 0) { @@ -1309,6 +1389,10 @@ public static RuntimeScalar unaryMinus(RuntimeScalar runtimeScalar) { * Called when 'use warnings "uninitialized"' is in effect. */ public static RuntimeScalar unaryMinusWarn(RuntimeScalar runtimeScalar) { + return unaryMinusWarnUnpropagated(runtimeScalar).propagateTaint(runtimeScalar); + } + + private static RuntimeScalar unaryMinusWarnUnpropagated(RuntimeScalar runtimeScalar) { // Check if object is eligible for overloading int blessId = blessedId(runtimeScalar); if (blessId < 0) { @@ -1346,6 +1430,10 @@ public static RuntimeScalar unaryMinusWarn(RuntimeScalar runtimeScalar) { } public static RuntimeScalar integer(RuntimeScalar arg1) { + return integerUnpropagated(arg1).propagateTaint(arg1); + } + + private static RuntimeScalar integerUnpropagated(RuntimeScalar arg1) { // Check if object is eligible for overloading int blessId = blessedId(arg1); if (blessId < 0) { diff --git a/src/main/java/org/perlonjava/runtime/operators/ModuleOperators.java b/src/main/java/org/perlonjava/runtime/operators/ModuleOperators.java index 6bf5f6a7b..3ab769a28 100644 --- a/src/main/java/org/perlonjava/runtime/operators/ModuleOperators.java +++ b/src/main/java/org/perlonjava/runtime/operators/ModuleOperators.java @@ -142,6 +142,7 @@ public static RuntimeScalar requireInPackage(RuntimeScalar runtimeScalar, String * @return Result of execution (undef on error, with $@ or $! set) */ private static RuntimeBase doFile(RuntimeScalar runtimeScalar, boolean setINC, boolean isRequire, int ctx) { + RuntimeScalar.checkTaint(runtimeScalar, isRequire ? "require" : "do"); // Clear error variables at start GlobalVariable.setGlobalVariable("main::@", ""); GlobalVariable.setGlobalVariable("main::!", ""); diff --git a/src/main/java/org/perlonjava/runtime/operators/Operator.java b/src/main/java/org/perlonjava/runtime/operators/Operator.java index f109e5a3e..12faf3777 100644 --- a/src/main/java/org/perlonjava/runtime/operators/Operator.java +++ b/src/main/java/org/perlonjava/runtime/operators/Operator.java @@ -54,6 +54,7 @@ public static RuntimeScalar chmod(RuntimeList runtimeList) { // Process each file in the flattened list for (RuntimeScalar fileScalar : fileList) { + RuntimeScalar.checkTaint(fileScalar, "chmod"); String fileName = fileScalar.toString(); Path resolved = RuntimeIO.resolvePath(fileName, "chmod"); if (resolved == null) { @@ -295,9 +296,17 @@ public static RuntimeList split(RuntimeScalar quotedRegex, RuntimeList args, int } } + if (GlobalContext.isTaintModeActive() && string.isTainted()) { + for (RuntimeBase element : splitElements) { + if (element instanceof RuntimeScalar scalar) { + scalar.tainted = true; + } + } + } + if (ctx == SCALAR) { int size = result.elements.size(); - return getScalarInt(size).getList(); + return getScalarInt(size).propagateTaint(string).getList(); } return result; } @@ -802,6 +811,7 @@ public static RuntimeBase repeat(RuntimeBase value, RuntimeScalar timesScalar, i scalarValue = value.scalar(); } RuntimeScalar rv = new RuntimeScalar(scalarValue.toString().repeat(Math.max(0, times))); + rv.formatPictureTainted = GlobalContext.isTaintModeActive() && timesScalar.isTainted(); if (scalarValue.type == RuntimeScalarType.BYTE_STRING) { rv.type = RuntimeScalarType.BYTE_STRING; } @@ -937,7 +947,7 @@ public static RuntimeScalar readlink(int ctx, RuntimeBase... args) { if (Files.isSymbolicLink(linkPath)) { Path targetPath = Files.readSymbolicLink(linkPath); - return new RuntimeScalar(targetPath.toString()); + return new RuntimeScalar(targetPath.toString()).taintFromExternalInput(); } else { getGlobalVariable("main::!").set("Invalid argument"); return RuntimeScalar.undef(); @@ -972,8 +982,12 @@ public static RuntimeScalar rename(int ctx, RuntimeBase... args) { throw new PerlCompilerException("Not enough arguments for rename"); } - String oldName = args[0].getFirst().toString(); - String newName = args[1].getFirst().toString(); + RuntimeScalar oldNameScalar = args[0].getFirst(); + RuntimeScalar newNameScalar = args[1].getFirst(); + RuntimeScalar.checkTaint(oldNameScalar, "rename"); + RuntimeScalar.checkTaint(newNameScalar, "rename"); + String oldName = oldNameScalar.toString(); + String newName = newNameScalar.toString(); try { Path oldPath = RuntimeIO.resolvePath(oldName); diff --git a/src/main/java/org/perlonjava/runtime/operators/Pack.java b/src/main/java/org/perlonjava/runtime/operators/Pack.java index c04f5230d..1a284d1e0 100644 --- a/src/main/java/org/perlonjava/runtime/operators/Pack.java +++ b/src/main/java/org/perlonjava/runtime/operators/Pack.java @@ -273,10 +273,15 @@ public static RuntimeScalar pack(RuntimeList args) { boolean shouldUpgrade = !result.byteModeUsed() && (result.hasUnicodeInNormalMode() || output.hasUnicodeCharacters()); - if (shouldUpgrade) { - return new RuntimeScalar(output.toUpgradedString()); + RuntimeScalar packed = shouldUpgrade + ? new RuntimeScalar(output.toUpgradedString()) + : new RuntimeScalar(output.toByteArray()); + for (RuntimeBase input : args.elements) { + if (input instanceof RuntimeScalar scalar) { + packed = packed.propagateTaint(scalar); + } } - return new RuntimeScalar(output.toByteArray()); + return packed; } public static PackResult packInto(String template, List values, int startValueIndex, diff --git a/src/main/java/org/perlonjava/runtime/operators/Readline.java b/src/main/java/org/perlonjava/runtime/operators/Readline.java index 7a0ddcc2f..639ca37ce 100644 --- a/src/main/java/org/perlonjava/runtime/operators/Readline.java +++ b/src/main/java/org/perlonjava/runtime/operators/Readline.java @@ -84,7 +84,7 @@ public static RuntimeScalar readline(RuntimeIO runtimeIO) { // the file contents (possibly the empty string) even if the // handle is positioned at EOF; the next call returns undef. if (runtimeIO.eof().getBoolean() && runtimeIO.currentLineNumber > 0) { - return scalarUndef; + return externalUndef(); } StringBuilder content = new StringBuilder(); boolean isByteData = true; @@ -165,7 +165,7 @@ private static RuntimeScalar readParagraphMode(RuntimeIO runtimeIO) { // Return undef if we've reached EOF and no characters were read (excluding skipped newlines) if (!inParagraph && runtimeIO.eof().getBoolean()) { - return scalarUndef; + return externalUndef(); } // Increment the line number counter once per paragraph read. @@ -195,7 +195,7 @@ private static RuntimeScalar readFixedLength(RuntimeIO runtimeIO, int length) { // Return undef if we've reached EOF and no characters were read if (result.length() == 0 && runtimeIO.eof().getBoolean()) { - return scalarUndef; + return externalUndef(); } // Don't increment line numbers for fixed-length reads @@ -229,7 +229,7 @@ private static RuntimeScalar readUntilCharacter(RuntimeIO runtimeIO, char separa // Return undef if we've reached EOF and no characters were read if (line.isEmpty() && runtimeIO.eof().getBoolean()) { - return scalarUndef; + return externalUndef(); } RuntimeScalar result = new RuntimeScalar(line.toString()); @@ -269,7 +269,7 @@ private static RuntimeScalar readUntilString(RuntimeIO runtimeIO, String separat // Return undef if we've reached EOF and no characters were read if (line.isEmpty() && runtimeIO.eof().getBoolean()) { - return scalarUndef; + return externalUndef(); } RuntimeScalar result = new RuntimeScalar(line.toString()); @@ -279,6 +279,10 @@ private static RuntimeScalar readUntilString(RuntimeIO runtimeIO, String separat return result.taintFromExternalInput(); } + private static RuntimeScalar externalUndef() { + return new RuntimeScalar().taintFromExternalInput(); + } + /** * Reads a specified number of characters from a file handle into a scalar. * diff --git a/src/main/java/org/perlonjava/runtime/operators/ScalarOperators.java b/src/main/java/org/perlonjava/runtime/operators/ScalarOperators.java index 64fab6051..aa2db017f 100644 --- a/src/main/java/org/perlonjava/runtime/operators/ScalarOperators.java +++ b/src/main/java/org/perlonjava/runtime/operators/ScalarOperators.java @@ -11,6 +11,10 @@ public class ScalarOperators { public static RuntimeScalar oct(RuntimeScalar runtimeScalar) { + return octUnpropagated(runtimeScalar).propagateTaint(runtimeScalar); + } + + private static RuntimeScalar octUnpropagated(RuntimeScalar runtimeScalar) { String expr = runtimeScalar.toString(); StringParser.assertNoWideCharacters(expr, "oct"); @@ -109,6 +113,10 @@ public static RuntimeScalar oct(RuntimeScalar runtimeScalar) { } public static RuntimeScalar ord(RuntimeScalar runtimeScalar) { + return ordUnpropagated(runtimeScalar).propagateTaint(runtimeScalar); + } + + private static RuntimeScalar ordUnpropagated(RuntimeScalar runtimeScalar) { String str = runtimeScalar.toString(); long i; if (str.isEmpty()) { @@ -127,6 +135,10 @@ public static RuntimeScalar ord(RuntimeScalar runtimeScalar) { * @return a RuntimeScalar containing the byte value (0-255) */ public static RuntimeScalar ordBytes(RuntimeScalar runtimeScalar) { + return ordBytesUnpropagated(runtimeScalar).propagateTaint(runtimeScalar); + } + + private static RuntimeScalar ordBytesUnpropagated(RuntimeScalar runtimeScalar) { // Regex capture variables are read-only proxies. Resolve the current // capture so its BYTE_STRING flag participates in byte-wise ord(). if (runtimeScalar instanceof ScalarSpecialVariable specialVariable) { @@ -159,6 +171,10 @@ public static RuntimeScalar ordBytes(RuntimeScalar runtimeScalar) { } public static RuntimeScalar hex(RuntimeScalar runtimeScalar) { + return hexUnpropagated(runtimeScalar).propagateTaint(runtimeScalar); + } + + private static RuntimeScalar hexUnpropagated(RuntimeScalar runtimeScalar) { String expr = runtimeScalar.toString(); long result = 0; boolean useDouble = false; diff --git a/src/main/java/org/perlonjava/runtime/operators/SprintfOperator.java b/src/main/java/org/perlonjava/runtime/operators/SprintfOperator.java index ce9da9d7f..b6122b5e6 100644 --- a/src/main/java/org/perlonjava/runtime/operators/SprintfOperator.java +++ b/src/main/java/org/perlonjava/runtime/operators/SprintfOperator.java @@ -44,6 +44,7 @@ public static RuntimeScalar sprintfBytes(RuntimeScalar runtimeScalar, RuntimeLis } private static RuntimeScalar sprintfInternal(RuntimeScalar runtimeScalar, RuntimeList list, boolean bytesMode) { + RuntimeScalar.checkTaint(runtimeScalar, "sprintf"); charsWritten = 0; // Reset counter // Expand the list to ensure all elements are available list = new RuntimeList((RuntimeBase) list); @@ -66,6 +67,7 @@ private static RuntimeScalar sprintfInternal(RuntimeScalar runtimeScalar, Runtim boolean hasValidSpecifier = false; // Track if we have any valid specifiers boolean hasPositionalParameter = false; // Track if any positional parameters are used boolean hasInvalidSpecifier = false; // Track if any invalid specifiers were found + boolean hasTaintedArgument = false; // Parse the format string into literals and format specifiers SprintfFormatParser.ParseResult parsed = SprintfFormatParser.parse(format); @@ -174,6 +176,9 @@ private static RuntimeScalar sprintfInternal(RuntimeScalar runtimeScalar, Runtim ProcessResult processResult = processFormatSpecifierTracked(spec, list, argIndex, formatter, bytesMode); result.append(processResult.formatted); charsWritten += processResult.formatted.length(); + if (GlobalContext.isTaintModeActive()) { + hasTaintedArgument |= usedArgumentIsTainted(spec, list, argIndex); + } // Only update maxArgIndexUsed if this specifier actually consumed arguments if (spec.conversionChar != '%' || spec.widthFromArg) { @@ -221,9 +226,47 @@ private static RuntimeScalar sprintfInternal(RuntimeScalar runtimeScalar, Runtim if (!hasUtf8Input) { res.type = RuntimeScalarType.BYTE_STRING; } + if (hasTaintedArgument) { + res.tainted = true; + } return res; } + private static boolean usedArgumentIsTainted(FormatSpecifier spec, RuntimeList list, int argIndex) { + java.util.LinkedHashSet used = new java.util.LinkedHashSet<>(); + int current = argIndex; + + if (spec.vectorFlag && spec.widthFromArg && spec.raw.matches(".*\\*v.*")) { + used.add(current++); // vector separator + if (spec.precisionFromArg) { + used.add(current++); // vector element width + } + used.add(current); // vector value + } else { + if (spec.widthFromArg) { + used.add(spec.widthArgIndex != null ? spec.widthArgIndex - 1 : current++); + } + if (spec.precisionFromArg) { + used.add(spec.precisionArgIndex != null ? spec.precisionArgIndex - 1 : current++); + } + if (spec.conversionChar != '%') { + used.add(spec.parameterIndex != null ? spec.parameterIndex - 1 : current); + } + if (spec.vectorFlag && spec.separatorArgIndex != null) { + used.add(spec.separatorArgIndex - 1); + } + } + + for (int index : used) { + if (index >= 0 && index < list.size() + && list.elements.get(index) instanceof RuntimeScalar scalar + && scalar.isTainted()) { + return true; + } + } + return false; + } + private static void handlePercentN(FormatSpecifier spec, RuntimeList list, int argIndex) { int targetIndex = spec.parameterIndex != null ? spec.parameterIndex - 1 : argIndex; diff --git a/src/main/java/org/perlonjava/runtime/operators/StringOperators.java b/src/main/java/org/perlonjava/runtime/operators/StringOperators.java index 1a593e39c..4fa29732b 100644 --- a/src/main/java/org/perlonjava/runtime/operators/StringOperators.java +++ b/src/main/java/org/perlonjava/runtime/operators/StringOperators.java @@ -31,6 +31,10 @@ private static boolean bytesHintActive() { * @return a {@link RuntimeScalar} containing the length of the input as an integer */ public static RuntimeScalar length(RuntimeScalar runtimeScalar) { + return lengthUnpropagated(runtimeScalar).propagateTaint(runtimeScalar); + } + + private static RuntimeScalar lengthUnpropagated(RuntimeScalar runtimeScalar) { // If the scalar is undefined, return undef if (!runtimeScalar.getDefinedBoolean()) { return RuntimeScalarCache.scalarUndef; @@ -48,6 +52,10 @@ public static RuntimeScalar length(RuntimeScalar runtimeScalar) { * @return a {@link RuntimeScalar} containing the byte length of the input */ public static RuntimeScalar lengthBytes(RuntimeScalar runtimeScalar) { + return lengthBytesUnpropagated(runtimeScalar).propagateTaint(runtimeScalar); + } + + private static RuntimeScalar lengthBytesUnpropagated(RuntimeScalar runtimeScalar) { // If the scalar is undefined, return undef if (!runtimeScalar.getDefinedBoolean()) { return RuntimeScalarCache.scalarUndef; @@ -174,6 +182,10 @@ private static boolean perlQuotemetaMustQuote(int cp, RuntimeScalar runtimeScala * @return a {@link RuntimeScalar} with the case-folded string */ public static RuntimeScalar fc(RuntimeScalar runtimeScalar) { + return fcUnpropagated(runtimeScalar).propagateTaint(runtimeScalar); + } + + private static RuntimeScalar fcUnpropagated(RuntimeScalar runtimeScalar) { if (runtimeScalar.type == RuntimeScalarType.BYTE_STRING) { return caseFoldBytesAsciiOnly(runtimeScalar); } @@ -185,6 +197,10 @@ public static RuntimeScalar fc(RuntimeScalar runtimeScalar) { * This is used under the unicode_strings feature. */ public static RuntimeScalar fcUnicode(RuntimeScalar runtimeScalar) { + return fcUnicodeUnpropagated(runtimeScalar).propagateTaint(runtimeScalar); + } + + private static RuntimeScalar fcUnicodeUnpropagated(RuntimeScalar runtimeScalar) { String str = runtimeScalar.toString(); // Perform full Unicode case folding using ICU4J CaseMap // Note: We do NOT use NFKC normalization because Perl's fc() preserves @@ -203,6 +219,10 @@ public static RuntimeScalar fcUnicode(RuntimeScalar runtimeScalar) { * @return a {@link RuntimeScalar} with the case-folded bytes */ public static RuntimeScalar fcBytes(RuntimeScalar runtimeScalar) { + return fcBytesUnpropagated(runtimeScalar).propagateTaint(runtimeScalar); + } + + private static RuntimeScalar fcBytesUnpropagated(RuntimeScalar runtimeScalar) { // Under 'use bytes', we operate on the UTF-8 bytes of the input RuntimeScalar asBytes = toUtf8Bytes(runtimeScalar); // Case-fold only ASCII bytes (A-Z -> a-z), leave others unchanged @@ -217,6 +237,10 @@ public static RuntimeScalar fcBytes(RuntimeScalar runtimeScalar) { * @return a {@link RuntimeScalar} with the lowercase string */ public static RuntimeScalar lc(RuntimeScalar runtimeScalar) { + return lcUnpropagated(runtimeScalar).propagateTaint(runtimeScalar); + } + + private static RuntimeScalar lcUnpropagated(RuntimeScalar runtimeScalar) { if (runtimeScalar.type == RuntimeScalarType.BYTE_STRING) { return caseFoldBytesAsciiOnly(runtimeScalar); } @@ -228,6 +252,10 @@ public static RuntimeScalar lc(RuntimeScalar runtimeScalar) { * This is used under the unicode_strings feature. */ public static RuntimeScalar lcUnicode(RuntimeScalar runtimeScalar) { + return lcUnicodeUnpropagated(runtimeScalar).propagateTaint(runtimeScalar); + } + + private static RuntimeScalar lcUnicodeUnpropagated(RuntimeScalar runtimeScalar) { // Convert the string to lowercase using ICU4J for proper Unicode handling String str = UCharacter.toLowerCase(runtimeScalar.toString()); return makeStringResult(str, runtimeScalar); @@ -238,6 +266,10 @@ public static RuntimeScalar lcUnicode(RuntimeScalar runtimeScalar) { * Operates on the UTF-8 bytes of the input, only affecting ASCII. */ public static RuntimeScalar lcBytes(RuntimeScalar runtimeScalar) { + return lcBytesUnpropagated(runtimeScalar).propagateTaint(runtimeScalar); + } + + private static RuntimeScalar lcBytesUnpropagated(RuntimeScalar runtimeScalar) { RuntimeScalar asBytes = toUtf8Bytes(runtimeScalar); return caseFoldBytesAsciiOnly(asBytes); } @@ -250,6 +282,10 @@ public static RuntimeScalar lcBytes(RuntimeScalar runtimeScalar) { * @return a {@link RuntimeScalar} with the first character in lowercase */ public static RuntimeScalar lcfirst(RuntimeScalar runtimeScalar) { + return lcfirstUnpropagated(runtimeScalar).propagateTaint(runtimeScalar); + } + + private static RuntimeScalar lcfirstUnpropagated(RuntimeScalar runtimeScalar) { if (runtimeScalar.type == RuntimeScalarType.BYTE_STRING) { return lcfirstBytes(runtimeScalar); } @@ -261,6 +297,10 @@ public static RuntimeScalar lcfirst(RuntimeScalar runtimeScalar) { * This is used under the unicode_strings feature. */ public static RuntimeScalar lcfirstUnicode(RuntimeScalar runtimeScalar) { + return lcfirstUnicodeUnpropagated(runtimeScalar).propagateTaint(runtimeScalar); + } + + private static RuntimeScalar lcfirstUnicodeUnpropagated(RuntimeScalar runtimeScalar) { String str = runtimeScalar.toString(); // Check if the string is empty if (str.isEmpty()) { @@ -283,6 +323,10 @@ public static RuntimeScalar lcfirstUnicode(RuntimeScalar runtimeScalar) { * @return a {@link RuntimeScalar} with the uppercase string */ public static RuntimeScalar uc(RuntimeScalar runtimeScalar) { + return ucUnpropagated(runtimeScalar).propagateTaint(runtimeScalar); + } + + private static RuntimeScalar ucUnpropagated(RuntimeScalar runtimeScalar) { if (runtimeScalar.type == RuntimeScalarType.BYTE_STRING) { return uppercaseBytesAsciiOnly(runtimeScalar); } @@ -294,6 +338,10 @@ public static RuntimeScalar uc(RuntimeScalar runtimeScalar) { * This is used under the unicode_strings feature. */ public static RuntimeScalar ucUnicode(RuntimeScalar runtimeScalar) { + return ucUnicodeUnpropagated(runtimeScalar).propagateTaint(runtimeScalar); + } + + private static RuntimeScalar ucUnicodeUnpropagated(RuntimeScalar runtimeScalar) { // Convert the string to uppercase using ICU4J for proper Unicode handling String str = UCharacter.toUpperCase(runtimeScalar.toString()); return makeStringResult(str, runtimeScalar); @@ -308,6 +356,10 @@ public static RuntimeScalar ucUnicode(RuntimeScalar runtimeScalar) { * @return a {@link RuntimeScalar} with the first character in titlecase */ public static RuntimeScalar ucfirst(RuntimeScalar runtimeScalar) { + return ucfirstUnpropagated(runtimeScalar).propagateTaint(runtimeScalar); + } + + private static RuntimeScalar ucfirstUnpropagated(RuntimeScalar runtimeScalar) { if (runtimeScalar.type == RuntimeScalarType.BYTE_STRING) { return ucfirstBytes(runtimeScalar); } @@ -319,6 +371,10 @@ public static RuntimeScalar ucfirst(RuntimeScalar runtimeScalar) { * This is used under the unicode_strings feature. */ public static RuntimeScalar ucfirstUnicode(RuntimeScalar runtimeScalar) { + return ucfirstUnicodeUnpropagated(runtimeScalar).propagateTaint(runtimeScalar); + } + + private static RuntimeScalar ucfirstUnicodeUnpropagated(RuntimeScalar runtimeScalar) { String str = runtimeScalar.toString(); // Check if the string is empty if (str.isEmpty()) { @@ -351,6 +407,11 @@ public static RuntimeScalar ucfirstUnicode(RuntimeScalar runtimeScalar) { * @return a {@link RuntimeScalar} containing the index of the first occurrence, or -1 if not found */ public static RuntimeScalar index(RuntimeScalar runtimeScalar, RuntimeScalar substr, RuntimeScalar position) { + return indexUnpropagated(runtimeScalar, substr, position) + .propagateTaint(runtimeScalar, substr, position); + } + + private static RuntimeScalar indexUnpropagated(RuntimeScalar runtimeScalar, RuntimeScalar substr, RuntimeScalar position) { String str = runtimeScalar.toString(); String sub = substr.toString(); int pos = position.type == RuntimeScalarType.UNDEF @@ -392,6 +453,11 @@ public static RuntimeScalar index(RuntimeScalar runtimeScalar, RuntimeScalar sub * @return a {@link RuntimeScalar} containing the index of the last occurrence, or -1 if not found */ public static RuntimeScalar rindex(RuntimeScalar runtimeScalar, RuntimeScalar substr, RuntimeScalar position) { + return rindexUnpropagated(runtimeScalar, substr, position) + .propagateTaint(runtimeScalar, substr, position); + } + + private static RuntimeScalar rindexUnpropagated(RuntimeScalar runtimeScalar, RuntimeScalar substr, RuntimeScalar position) { String str = runtimeScalar.toString(); String sub = substr.toString(); int pos = position.type == RuntimeScalarType.UNDEF @@ -431,8 +497,8 @@ public static RuntimeScalar stringConcat(RuntimeScalar runtimeScalar, RuntimeSca RuntimeScalar overloaded = tryStringConcatOverload(runtimeScalar, b); if (overloaded != null) return overloaded; - RuntimeScalar aResolved = resolveTiedStringOperand(runtimeScalar); - RuntimeScalar bResolved = resolveTiedStringOperand(b); + RuntimeScalar aResolved = stringifyForStringContext(resolveTiedStringOperand(runtimeScalar)); + RuntimeScalar bResolved = stringifyForStringContext(resolveTiedStringOperand(b)); String bStr = bResolved.toString(); String aStr = aResolved.toString(); @@ -481,9 +547,12 @@ private static RuntimeScalar resolveTiedStringOperand(RuntimeScalar scalar) { private static RuntimeScalar propagateTaint(RuntimeScalar result, RuntimeScalar... inputs) { for (RuntimeScalar input : inputs) { + if (input != null && input.formatPictureTainted) { + result.formatPictureTainted = true; + result.tainted = true; + } if (input != null && input.isTainted()) { result.tainted = true; - break; } } return result; @@ -503,9 +572,9 @@ public static RuntimeScalar stringConcatWarnUninitialized(RuntimeScalar runtimeS // For tied variables, we must only FETCH once, then use the result for both // the definedness check and the actual concatenation. // First, resolve tied variables to get their actual values (triggers FETCH once per tied var) - RuntimeScalar aResolved = (runtimeScalar.type == RuntimeScalarType.TIED_SCALAR) + RuntimeScalar aResolved = (runtimeScalar.type == RuntimeScalarType.TIED_SCALAR) ? runtimeScalar.tiedFetch() : runtimeScalar; - RuntimeScalar bResolved = (b.type == RuntimeScalarType.TIED_SCALAR) + RuntimeScalar bResolved = (b.type == RuntimeScalarType.TIED_SCALAR) ? b.tiedFetch() : b; // Now check definedness on the resolved values (no additional FETCH) @@ -516,6 +585,9 @@ public static RuntimeScalar stringConcatWarnUninitialized(RuntimeScalar runtimeS RuntimeScalar overloaded = tryStringConcatOverload(aResolved, bResolved); if (overloaded != null) return overloaded; + + aResolved = stringifyForStringContext(aResolved); + bResolved = stringifyForStringContext(bResolved); // Get string values from resolved scalars String aStr = aResolved.toString(); @@ -726,6 +798,11 @@ public static RuntimeScalar join(RuntimeScalar runtimeScalar, RuntimeBase list) return joinInternal(runtimeScalar, list, true, false); } + private static RuntimeScalar recordJoinTaint(RuntimeScalar result) { + GlobalContext.setThreadJoinTaint(result.isTainted()); + return result; + } + /** * Internal join implementation with optional warning control. * Used for both explicit join() calls and string interpolation. @@ -754,7 +831,7 @@ private static RuntimeScalar joinInternal(RuntimeScalar runtimeScalar, RuntimeBa WarnDie.warnWithCategory(new RuntimeScalar("Use of uninitialized value in join or string"), RuntimeScalarCache.scalarEmptyString, "uninitialized"); } - return new RuntimeScalar(""); + return recordJoinTaint(new RuntimeScalar("")); } // Fast path: 1 element -> return that element (no separator evaluation needed) @@ -767,13 +844,13 @@ private static RuntimeScalar joinInternal(RuntimeScalar runtimeScalar, RuntimeBa WarnDie.warnWithCategory(new RuntimeScalar("Use of uninitialized value in join or string"), RuntimeScalarCache.scalarEmptyString, "uninitialized"); } - RuntimeScalar resolved = resolveTiedStringOperand(scalar); + RuntimeScalar resolved = stringifyForStringContext(resolveTiedStringOperand(scalar)); RuntimeScalar res = new RuntimeScalar(resolved.toString()); if (resolved.type != RuntimeScalarType.STRING) { res.type = BYTE_STRING; } res.tainted = resolved.isTainted(); - return res; + return recordJoinTaint(res); } // 2+ elements: evaluate the separator @@ -782,7 +859,7 @@ private static RuntimeScalar joinInternal(RuntimeScalar runtimeScalar, RuntimeBa RuntimeScalarCache.scalarEmptyString, "uninitialized"); } - RuntimeScalar separatorResolved = resolveTiedStringOperand(runtimeScalar); + RuntimeScalar separatorResolved = stringifyForStringContext(resolveTiedStringOperand(runtimeScalar)); String delimiter = separatorResolved.toString(); // In Perl, join produces a byte-string unless one of the inputs has @@ -807,7 +884,7 @@ private static RuntimeScalar joinInternal(RuntimeScalar runtimeScalar, RuntimeBa RuntimeScalarCache.scalarEmptyString, "uninitialized"); } - RuntimeScalar resolved = resolveTiedStringOperand(scalar); + RuntimeScalar resolved = stringifyForStringContext(resolveTiedStringOperand(scalar)); if (resolved.type == RuntimeScalarType.STRING) { hasUtf8 = true; } @@ -819,7 +896,11 @@ private static RuntimeScalar joinInternal(RuntimeScalar runtimeScalar, RuntimeBa res.type = BYTE_STRING; } res.tainted = tainted; - return res; + return recordJoinTaint(res); + } + + private static RuntimeScalar stringifyForStringContext(RuntimeScalar scalar) { + return RuntimeScalarType.blessedId(scalar) != 0 ? Overload.stringify(scalar) : scalar; } /** diff --git a/src/main/java/org/perlonjava/runtime/operators/SystemOperator.java b/src/main/java/org/perlonjava/runtime/operators/SystemOperator.java index e5197bf42..1255b8af0 100644 --- a/src/main/java/org/perlonjava/runtime/operators/SystemOperator.java +++ b/src/main/java/org/perlonjava/runtime/operators/SystemOperator.java @@ -14,6 +14,10 @@ import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.nio.file.attribute.PosixFilePermission; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -36,6 +40,7 @@ public class SystemOperator { // Shell syntax that prevents Perl's one-string command direct-exec fast path. private static final Pattern DIRECT_COMMAND_SHELL_METACHARACTERS = Pattern.compile("[*?\\[\\]{}()<>|&;`'\"\\$%]"); + private static final Pattern TAINTED_ENV_METACHARACTERS = Pattern.compile("[^A-Za-z0-9_./-]"); private static String decodeSubprocessOutput(byte[] bytes) { StringBuilder decoded = new StringBuilder(bytes.length); @@ -124,6 +129,9 @@ public static RuntimeBase systemCommand(RuntimeScalar command, int ctx) { } } + RuntimeScalar.checkTaint(command, "``"); + checkTaintEnvironment(); + String cmd = command.toString(); CommandResult result; @@ -159,6 +167,8 @@ public static RuntimeBase systemCommand(RuntimeScalar command, int ctx) { * @throws PerlCompilerException if an error occurs during command execution. */ public static RuntimeScalar system(RuntimeList args, boolean hasHandle, int ctx) { + checkTaintArguments(args, "system"); + checkTaintEnvironment(); // Flatten the arguments - arrays and lists should be expanded to individual elements List flattenedArgs = flattenToStringList(args.elements); @@ -235,6 +245,93 @@ private static List flattenToStringList(List elements) { return result; } + private static void checkTaintArguments(RuntimeList args, String operation) { + if (!GlobalContext.isTaintModeActive()) { + return; + } + for (RuntimeBase value : args.elements) { + checkTaintValue(value, operation); + } + } + + private static void checkTaintValue(RuntimeBase value, String operation) { + if (value instanceof RuntimeScalar scalar) { + RuntimeScalar.checkTaint(scalar, operation); + } else if (value instanceof RuntimeList list) { + for (RuntimeBase element : list.elements) { + checkTaintValue(element, operation); + } + } else if (value instanceof RuntimeArray array) { + for (RuntimeScalar element : array.elements) { + RuntimeScalar.checkTaint(element, operation); + } + } + } + + private static void checkTaintEnvironment() { + if (!GlobalContext.isTaintModeActive()) { + return; + } + RuntimeHash env = GlobalVariable.getGlobalHash("main::ENV"); + if (env.taintEnvironmentAliasDescription != null) { + throw new PerlCompilerException( + "%ENV is aliased to " + env.taintEnvironmentAliasDescription + + " while running with -T switch"); + } + for (String name : List.of("PATH", "IFS", "CDPATH", "ENV", "BASH_ENV")) { + RuntimeScalar value = env.elements.get(name); + if (value != null && value.isTainted()) { + throw new PerlCompilerException( + "Insecure $ENV{" + name + "} while running with -T switch"); + } + } + + RuntimeScalar path = env.elements.get("PATH"); + if (path != null && path.getDefinedBoolean()) { + checkPathDirectories(path.toString()); + } + + RuntimeScalar term = env.elements.get("TERM"); + if (term != null && term.isTainted() + && TAINTED_ENV_METACHARACTERS.matcher(term.toString()).find()) { + throw new PerlCompilerException( + "Insecure $ENV{TERM} while running with -T switch"); + } + } + + private static void checkPathDirectories(String pathValue) { + if (SystemUtils.osIsWindows()) { + return; + } + + for (String directory : pathValue.split(":", -1)) { + try { + Path path = Path.of(directory); + if (directory.isEmpty() || !path.isAbsolute() || isWorldWritableDirectory(path)) { + throw insecurePathException(); + } + } catch (InvalidPathException e) { + throw insecurePathException(); + } + } + } + + private static boolean isWorldWritableDirectory(Path path) { + if (!Files.isDirectory(path)) { + return false; + } + try { + return Files.getPosixFilePermissions(path).contains(PosixFilePermission.OTHERS_WRITE); + } catch (UnsupportedOperationException | IOException | SecurityException e) { + return false; + } + } + + private static PerlCompilerException insecurePathException() { + return new PerlCompilerException( + "Insecure directory in $ENV{PATH} while running with -T switch"); + } + private static List splitDirectCommandWords(String command) { String trimmed = command.trim(); if (trimmed.isEmpty()) { @@ -875,21 +972,22 @@ private static RuntimeBase processOutput(String output, int ctx) { int separatorLength = separator.length(); if (separatorLength == 0) { - result.add(new RuntimeScalar(output)); + result.add(new RuntimeScalar(output).taintFromExternalInput()); } else { while (index < output.length()) { int nextIndex = output.indexOf(separator, index); if (nextIndex == -1) { - result.add(new RuntimeScalar(output.substring(index))); + result.add(new RuntimeScalar(output.substring(index)).taintFromExternalInput()); break; } - result.add(new RuntimeScalar(output.substring(index, nextIndex + separatorLength))); + result.add(new RuntimeScalar(output.substring(index, nextIndex + separatorLength)) + .taintFromExternalInput()); index = nextIndex + separatorLength; } } return list; } else { - return new RuntimeScalar(output); + return new RuntimeScalar(output).taintFromExternalInput(); } } @@ -905,6 +1003,8 @@ private static RuntimeBase processOutput(String output, int ctx) { * @throws PerlCompilerException if an error occurs during command execution. */ public static RuntimeScalar exec(RuntimeList args, boolean hasHandle, int ctx) { + checkTaintArguments(args, "exec"); + checkTaintEnvironment(); // Flatten the arguments - arrays and lists should be expanded to individual elements List flattenedArgs = flattenToStringList(args.elements); diff --git a/src/main/java/org/perlonjava/runtime/operators/UnlinkOperator.java b/src/main/java/org/perlonjava/runtime/operators/UnlinkOperator.java index d46c9d1e7..16674335e 100644 --- a/src/main/java/org/perlonjava/runtime/operators/UnlinkOperator.java +++ b/src/main/java/org/perlonjava/runtime/operators/UnlinkOperator.java @@ -35,6 +35,7 @@ public static RuntimeBase unlink(int ctx, RuntimeBase... args) { } for (RuntimeScalar fileScalar : fileList) { + RuntimeScalar.checkTaint(fileScalar, "unlink"); String fileName = fileScalar.toString(); if (deleteFile(fileName)) { diff --git a/src/main/java/org/perlonjava/runtime/operators/UtimeOperator.java b/src/main/java/org/perlonjava/runtime/operators/UtimeOperator.java index 2bbe38ed9..3abb6a7e3 100644 --- a/src/main/java/org/perlonjava/runtime/operators/UtimeOperator.java +++ b/src/main/java/org/perlonjava/runtime/operators/UtimeOperator.java @@ -26,6 +26,10 @@ public static RuntimeScalar utime(int ctx, RuntimeBase... args) { } } + for (RuntimeScalar scalar : flat) { + RuntimeScalar.checkTaint(scalar, "utime"); + } + if (flat.size() < 3) { return new RuntimeScalar(0); } diff --git a/src/main/java/org/perlonjava/runtime/operators/Vec.java b/src/main/java/org/perlonjava/runtime/operators/Vec.java index 1fd910581..0ae0e13de 100644 --- a/src/main/java/org/perlonjava/runtime/operators/Vec.java +++ b/src/main/java/org/perlonjava/runtime/operators/Vec.java @@ -1,5 +1,6 @@ package org.perlonjava.runtime.operators; +import org.perlonjava.runtime.runtimetypes.GlobalContext; import org.perlonjava.runtime.runtimetypes.PerlCompilerException; import org.perlonjava.runtime.runtimetypes.RuntimeList; import org.perlonjava.runtime.runtimetypes.RuntimeScalar; @@ -30,7 +31,7 @@ public static RuntimeScalar vec(RuntimeList args) throws PerlCompilerException { // Return 0 for undefined values without autovivifying int offset = ((RuntimeScalar) args.elements.get(1)).getInt(); int bits = ((RuntimeScalar) args.elements.get(2)).getInt(); - return new RuntimeVecLvalue(strScalar, offset, bits, 0); + return vecResult(strScalar, offset, bits, 0); } String str = strScalar.toString(); @@ -52,14 +53,14 @@ public static RuntimeScalar vec(RuntimeList args) throws PerlCompilerException { // Handle negative offset if (offset < 0) { - return new RuntimeVecLvalue(strScalar, offset, bits, 0); + return vecResult(strScalar, offset, bits, 0); } // Check for potential overflow in offset * bits calculation // Use long arithmetic to detect overflow long longByteOffset = ((long) offset * bits) / 8; if (longByteOffset > Integer.MAX_VALUE || longByteOffset >= data.length) { - return new RuntimeVecLvalue(strScalar, offset, bits, 0); + return vecResult(strScalar, offset, bits, 0); } int byteOffset = (int) longByteOffset; @@ -69,16 +70,16 @@ public static RuntimeScalar vec(RuntimeList args) throws PerlCompilerException { if (bits == 64 && byteOffset + 8 <= data.length) { long longValue = buffer.getLong(byteOffset); - return new RuntimeVecLvalue(strScalar, offset, bits, longValue); + return vecResult(strScalar, offset, bits, longValue); } else if (bits == 32 && byteOffset + 4 <= data.length) { long unsignedValue = Integer.toUnsignedLong(buffer.getInt(byteOffset)); - return new RuntimeVecLvalue(strScalar, offset, bits, unsignedValue); + return vecResult(strScalar, offset, bits, unsignedValue); } else if (bits == 16 && byteOffset + 2 <= data.length) { int value = buffer.getShort(byteOffset) & 0xFFFF; - return new RuntimeVecLvalue(strScalar, offset, bits, value); + return vecResult(strScalar, offset, bits, value); } else if (bits == 8 && byteOffset < data.length) { int value = buffer.get(byteOffset) & 0xFF; - return new RuntimeVecLvalue(strScalar, offset, bits, value); + return vecResult(strScalar, offset, bits, value); } else { int value = 0; for (int i = 0; i < bits; i++) { @@ -88,10 +89,16 @@ public static RuntimeScalar vec(RuntimeList args) throws PerlCompilerException { value |= ((data[byteIndex] >> bitIndex) & 1) << i; } } - return new RuntimeVecLvalue(strScalar, offset, bits, value); + return vecResult(strScalar, offset, bits, value); } } + private static RuntimeVecLvalue vecResult(RuntimeScalar source, int offset, int bits, long value) { + RuntimeVecLvalue result = new RuntimeVecLvalue(source, offset, bits, value); + result.tainted = GlobalContext.isTaintModeActive() && source.isTainted(); + return result; + } + /** * Sets a bit field in a string to a specified value. * @@ -169,4 +176,4 @@ public static RuntimeScalar set(RuntimeList args, RuntimeScalar value) throws Pe ((RuntimeScalar) args.elements.getFirst()).set(new String(data, StandardCharsets.ISO_8859_1)); return value; } -} \ No newline at end of file +} diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/Re.java b/src/main/java/org/perlonjava/runtime/perlmodule/Re.java index 81e5b35ba..dd5acfb80 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/Re.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/Re.java @@ -166,6 +166,8 @@ public static RuntimeList importRe(RuntimeArray args, int ctx) { Warnings.warningManager.enableWarning("experimental::vlb"); } else if (opt.equalsIgnoreCase("eval")) { symbolTable.enableStrictOption(Strict.HINT_RE_EVAL); + } else if (opt.equalsIgnoreCase("taint")) { + symbolTable.enableStrictOption(Strict.HINT_RE_TAINT); } else if (opt.equals("/a")) { // use re '/a' - ASCII-restrict regex character classes symbolTable.enableStrictOption(Strict.HINT_RE_ASCII); @@ -199,6 +201,8 @@ public static RuntimeList unimportRe(RuntimeArray args, int ctx) { Warnings.warningManager.disableWarning("experimental::vlb"); } else if (opt.equalsIgnoreCase("eval")) { symbolTable.disableStrictOption(Strict.HINT_RE_EVAL); + } else if (opt.equalsIgnoreCase("taint")) { + symbolTable.disableStrictOption(Strict.HINT_RE_TAINT); } else if (opt.equals("/a") || opt.equals("/aa")) { symbolTable.disableStrictOption(Strict.HINT_RE_ASCII | Strict.HINT_RE_ASCII_AA); } else if (opt.equals("/u")) { diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/Strict.java b/src/main/java/org/perlonjava/runtime/perlmodule/Strict.java index debd92f2e..f0cd16660 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/Strict.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/Strict.java @@ -39,6 +39,7 @@ public class Strict extends PerlModuleBase { public static final int HINT_RE_UNICODE = 0x02000000; // use re '/u' public static final int HINT_RE_ASCII_AA = 0x04000000; // use re '/aa' public static final int HINT_RE_EVAL = 0x08000000; // use re 'eval' + public static final int HINT_RE_TAINT = 0x10000000; // use re 'taint' /** * Constructor for Strict. diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/Utf8.java b/src/main/java/org/perlonjava/runtime/perlmodule/Utf8.java index 7b91df75b..8a94299a1 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/Utf8.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/Utf8.java @@ -108,6 +108,7 @@ public static RuntimeList upgrade(RuntimeArray args, int ctx) { throw new IllegalStateException("Bad number of arguments for upgrade() method"); } RuntimeScalar scalar = args.get(0); + boolean wasTainted = GlobalContext.isTaintModeActive() && scalar.isTainted(); String string = scalar.toString(); byte[] utf8Bytes = string.getBytes(StandardCharsets.UTF_8); @@ -125,6 +126,7 @@ public static RuntimeList upgrade(RuntimeArray args, int ctx) { // that already contains Unicode code points > 0xFF (e.g. "\x{100}"). // This is fine — we just flip the type and preserve the content as-is. scalar.set(string); + scalar.tainted = wasTainted; scalar.type = STRING; } else if (scalar.type != STRING) { // Other types (INTEGER, DOUBLE, UNDEF, etc.): convert to string and mark as STRING. @@ -140,6 +142,7 @@ public static RuntimeList upgrade(RuntimeArray args, int ctx) { // WARNING: Do NOT skip this set() call, as it will cause regressions where // utf8::upgrade() corrupts Unicode strings to wrong values (e.g., U+0100 -> U+0000). scalar.set(string); + scalar.tainted = wasTainted; scalar.type = STRING; } // If scalar.type == STRING: already upgraded, do nothing. @@ -162,6 +165,7 @@ public static RuntimeList downgrade(RuntimeArray args, int ctx) { throw new IllegalStateException("Bad number of arguments for downgrade() method"); } RuntimeScalar scalar = args.get(0); + boolean wasTainted = GlobalContext.isTaintModeActive() && scalar.isTainted(); boolean failOk = args.size() == 2 && args.get(1).getBoolean(); String string = scalar.toString(); @@ -189,6 +193,7 @@ public static RuntimeList downgrade(RuntimeArray args, int ctx) { } catch (PerlCompilerException e) { scalar.value = decodedBytes; } + scalar.tainted = wasTainted; scalar.type = BYTE_STRING; } return new RuntimeScalar(true).getList(); @@ -220,9 +225,11 @@ public static RuntimeList encode(RuntimeArray args, int ctx) { throw new IllegalStateException("Bad number of arguments for encode() method"); } RuntimeScalar scalar = args.get(0); + boolean wasTainted = GlobalContext.isTaintModeActive() && scalar.isTainted(); String string = scalar.toString(); byte[] utf8Bytes = string.getBytes(StandardCharsets.UTF_8); scalar.set(new String(utf8Bytes, StandardCharsets.ISO_8859_1)); + scalar.tainted = wasTainted; scalar.type = BYTE_STRING; return new RuntimeScalar().getList(); } @@ -239,6 +246,7 @@ public static RuntimeList decode(RuntimeArray args, int ctx) { throw new IllegalStateException("Bad number of arguments for decode() method"); } RuntimeScalar scalar = args.get(0); + boolean wasTainted = GlobalContext.isTaintModeActive() && scalar.isTainted(); String string = scalar.toString(); // utf8::decode expects octet data (0-255). If the string contains @@ -265,6 +273,7 @@ public static RuntimeList decode(RuntimeArray args, int ctx) { CharBuffer decoded = decoder.decode(ByteBuffer.wrap(bytes)); String decodedStr = decoded.toString(); scalar.set(decodedStr); + scalar.tainted = wasTainted; // Per Perl 5 docs: "The UTF-8 flag is turned on only if the string // contains a multi-byte UTF-8 character (i.e., any char above 0x7F // after decoding)." For pure ASCII input (all chars <= 0x7F), the diff --git a/src/main/java/org/perlonjava/runtime/regex/RegexFlags.java b/src/main/java/org/perlonjava/runtime/regex/RegexFlags.java index de975c1e5..7d50ed303 100644 --- a/src/main/java/org/perlonjava/runtime/regex/RegexFlags.java +++ b/src/main/java/org/perlonjava/runtime/regex/RegexFlags.java @@ -25,7 +25,7 @@ public record RegexFlags(boolean isGlobalMatch, boolean keepCurrentPosition, boo boolean isMatchExactlyOnce, boolean useGAssertion, boolean isExtendedWhitespace, boolean isNonCapturing, boolean isOptimized, boolean isCaseInsensitive, boolean isMultiLine, boolean isDotAll, boolean isExtended, boolean preservesMatch, boolean isUnicode, - boolean isAscii, boolean allowEvalGroup) { + boolean isAscii, boolean allowEvalGroup, boolean taintResults) { public static RegexFlags fromModifiers(String modifiers, String patternString) { // m?PAT? is encoded by StringParser as an extra trailing '?' on the modifier string @@ -48,13 +48,14 @@ public static RegexFlags fromModifiers(String modifiers, String patternString) { modifiers.contains("p"), modifiers.contains("u"), modifiers.contains("a"), - modifiers.contains("E") + modifiers.contains("E"), + modifiers.contains("T") ); } public static void validateModifiers(String modifiers) { // Valid modifiers based on what's actually handled in fromModifiers - String validModifiers = "gcr?noimsxpadeulE"; // Add 'xx' handling separately, 'l' for locale, 'E' for internal re eval + String validModifiers = "gcr?noimsxpadeulET"; // E/T are internal lexical flags for (int i = 0; i < modifiers.length(); i++) { char modifier = modifiers.charAt(i); @@ -149,7 +150,8 @@ public RegexFlags with(String positiveFlags, String negativeFlags) { newPreservesMatch, newIsUnicode, newIsAscii, - this.allowEvalGroup + this.allowEvalGroup, + this.taintResults ); } @@ -166,6 +168,7 @@ public String toFlagString() { if (isExtended) flagString.append('x'); if (isNonCapturing) flagString.append('n'); if (isNonDestructive) flagString.append('r'); + if (taintResults) flagString.append('T'); return flagString.toString(); } diff --git a/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java b/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java index af7d74cfe..b0112d574 100644 --- a/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java +++ b/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java @@ -38,6 +38,8 @@ public class RuntimeRegex extends RuntimeBase implements RuntimeScalarReference private static final int CASE_INSENSITIVE = Pattern.CASE_INSENSITIVE; private static final int MULTILINE = Pattern.MULTILINE; private static final int DOTALL = Pattern.DOTALL; + private static final Pattern USER_DEFINED_PROPERTY_PATTERN = + Pattern.compile("\\\\([pP])\\{((?:[A-Za-z_][A-Za-z0-9_]*::)*(?:[Ii][sS]|[Ii][nN])[A-Za-z0-9_]*)}"); // Maximum size for the regex cache private static final int MAX_REGEX_CACHE_SIZE = 1000; // Cache to store compiled regex patterns @@ -73,6 +75,7 @@ protected boolean removeEldestEntry(Map.Entry eldest) { // Track whether the last successful match was on a BYTE_STRING input, // so that captures ($1, $2, $&, etc.) preserve BYTE_STRING type. public static boolean lastMatchWasByteString = false; + public static boolean lastMatchResultsTainted = false; public static int[] manualCaptureStarts = null; public static int[] manualCaptureEnds = null; // Compiled regex pattern (for byte strings - ASCII-only \w, \d) @@ -872,6 +875,8 @@ public static RuntimeScalar getQuotedRegex(RuntimeScalar patternString, RuntimeS // Unwrap readonly scalar if (patternString.type == RuntimeScalarType.READONLY_SCALAR) patternString = (RuntimeScalar) patternString.value; + validateTaintedPatternSecurity(patternString); + // Check if patternString is already a compiled regex if (patternString.type == RuntimeScalarType.REGEX) { RuntimeRegex originalRegex = (RuntimeRegex) patternString.value; @@ -896,7 +901,7 @@ public static RuntimeScalar getQuotedRegex(RuntimeScalar patternString, RuntimeS regex.patternFlags = regex.regexFlags.toPatternFlags(); regex.refCount = 0; // Track for proper weak ref handling - return new RuntimeScalar(regex); + return new RuntimeScalar(regex).propagateTaint(patternString); } // Check for qr overloading @@ -929,20 +934,48 @@ public static RuntimeScalar getQuotedRegex(RuntimeScalar patternString, RuntimeS regex.patternFlags = regex.regexFlags.toPatternFlags(); regex.refCount = 0; // Track for proper weak ref handling - return new RuntimeScalar(regex); + return new RuntimeScalar(regex).propagateTaint(patternString, overloadedResult); } // Try fallback to string conversion RuntimeScalar fallbackResult = overloadCtx.tryOverloadFallback(patternString, "(\"\""); if (fallbackResult != null) { - return new RuntimeScalar(compile(fallbackResult.toString(), modifierStr).cloneTracked()); + return new RuntimeScalar(compile(fallbackResult.toString(), modifierStr).cloneTracked()) + .propagateTaint(patternString, fallbackResult); } } } // Default: compile as string (cloneTracked() creates a tracked copy // so the cached RuntimeRegex is not corrupted by refCount changes) - return new RuntimeScalar(compile(patternString.toString(), modifierStr).cloneTracked()); + return new RuntimeScalar(compile(patternString.toString(), modifierStr).cloneTracked()) + .propagateTaint(patternString); + } + + private static void validateTaintedPatternSecurity(RuntimeScalar patternString) { + if (!GlobalContext.isTaintModeActive() || patternString == null + || !patternString.isTainted() || patternString.type == RuntimeScalarType.REGEX) { + return; + } + + String pattern = patternString.toString(); + if (pattern.contains("(?{") || pattern.contains("(??{")) { + throw new PerlCompilerException("Eval-group in insecure regular expression"); + } + + Matcher matcher = USER_DEFINED_PROPERTY_PATTERN.matcher(pattern); + if (!matcher.find()) { + return; + } + + String property = matcher.group(2); + String qualified = property.contains("::") ? property : "main::" + property; + if (GlobalVariable.isGlobalCodeRefDefined(qualified)) { + throw new PerlCompilerException( + "Insecure user-defined property \"" + property + "\" in regex"); + } + throw new PerlCompilerException( + "Insecure user-defined property \\" + matcher.group(1) + "{" + qualified + "}"); } /** @@ -1053,7 +1086,7 @@ public static RuntimeScalar getReplacementRegex(RuntimeScalar patternString, Run } regex.replacement = replacement; - return new RuntimeScalar(regex); + return new RuntimeScalar(regex).propagateTaint(patternString); } /** @@ -1405,6 +1438,9 @@ private static RuntimeBase matchRegexDirect(RuntimeScalar quotedRegex, RuntimeSc } found = true; + lastMatchResultsTainted = GlobalContext.isTaintModeActive() + && (quotedRegex.isTainted() + || (regex.regexFlags.taintResults() && string.isTainted())); lastMatchWasByteString = (string.type == RuntimeScalarType.BYTE_STRING); int captureCount = matcher.groupCount(); @@ -1987,8 +2023,10 @@ private static void setSubstitutionRegion(Matcher matcher, int start, int end, b } private static void updateReplacementMatchState(RuntimeRegex regex, Matcher matcher, - String inputStr, RuntimeScalar string) { + String inputStr, RuntimeScalar string, + boolean resultsTainted) { lastMatchWasByteString = (string.type == RuntimeScalarType.BYTE_STRING); + lastMatchResultsTainted = resultsTainted; // Initialize $1, $2, @+, @- only when we have a match globalMatcher = matcher; @@ -2033,9 +2071,15 @@ private static void updateReplacementMatchState(RuntimeRegex regex, Matcher matc } public static RuntimeBase replaceRegex(RuntimeScalar quotedRegex, RuntimeScalar string, int ctx) { - // Convert the input string to a Java string - String inputStr = string.toString(); - boolean wasByteString = (string.type == RuntimeScalarType.BYTE_STRING); + // Resolve a tied target exactly once for all reads. Keep `string` as + // the lvalue used for STORE after the substitution. + RuntimeScalar inputValue = string.type == RuntimeScalarType.TIED_SCALAR + ? string.tiedFetch() : string; + String inputStr = inputValue.toString(); + boolean wasByteString = (inputValue.type == RuntimeScalarType.BYTE_STRING); + boolean taintMode = GlobalContext.isTaintModeActive(); + boolean inputTainted = taintMode && inputValue.isTainted(); + boolean patternTainted = taintMode && quotedRegex.isTainted(); boolean resultNeedsUtf8 = !wasByteString; // Extract the regex pattern from the quotedRegex object @@ -2099,7 +2143,7 @@ public static RuntimeBase replaceRegex(RuntimeScalar quotedRegex, RuntimeScalar regex.emitWarningsOnUse(); - Pattern pattern = regex.selectPattern(string, inputStr); + Pattern pattern = regex.selectPattern(inputValue, inputStr); // Select appropriate pattern based on string's UTF-8 flag (same logic as matchRegex) if (pattern == regex.pattern && regex.patternUnicode != null && regex.patternUnicode != regex.pattern) { @@ -2109,9 +2153,9 @@ public static RuntimeBase replaceRegex(RuntimeScalar quotedRegex, RuntimeScalar } else if (hasInlineAsciiModifier(regex.patternString)) { // Inline (?a...) in pattern - use ASCII to be safe pattern = regex.pattern; - } else if (Utf8.isUtf8(string)) { + } else if (Utf8.isUtf8(inputValue)) { // UTF-8 string - use Unicode matching for \w, \d, \s semantics - pattern = regex.selectPattern(string, inputStr); + pattern = regex.selectPattern(inputValue, inputStr); } // else: BYTE_STRING - keep ASCII pattern (default) } @@ -2153,6 +2197,10 @@ public static RuntimeBase replaceRegex(RuntimeScalar quotedRegex, RuntimeScalar // Determine if the replacement is a code that needs to be evaluated boolean replacementIsCode = (replacement.type == RuntimeScalarType.CODE); + boolean replacementResultTainted = false; + boolean captureResultsTainted = patternTainted + || (regex.regexFlags.taintResults() && inputTainted); + boolean destructiveReplacement = !regex.regexFlags.isNonDestructive(); // Don't reset globalMatcher here - only reset it if we actually find a match // This preserves capture variables from previous matches when substitution doesn't match @@ -2172,7 +2220,7 @@ public static RuntimeBase replaceRegex(RuntimeScalar quotedRegex, RuntimeScalar } found++; - updateReplacementMatchState(regex, matcher, inputStr, string); + updateReplacementMatchState(regex, matcher, inputStr, inputValue, captureResultsTainted); String replacementStr; if (replacementIsCode) { @@ -2180,16 +2228,25 @@ public static RuntimeBase replaceRegex(RuntimeScalar quotedRegex, RuntimeScalar // Use callerArgs (the enclosing subroutine's @_) so $_[0] etc. work RuntimeArray args = (callerArgs != null) ? callerArgs : new RuntimeArray(); RuntimeList result = RuntimeCode.apply(replacement, args, RuntimeContextType.SCALAR); - if (Utf8.isUtf8(result.scalar())) { + RuntimeScalar replacementValue = stringifyReplacementValue(result.scalar()); + if (Utf8.isUtf8(replacementValue)) { resultNeedsUtf8 = true; } - replacementStr = result.toString(); + replacementResultTainted |= taintMode && replacementValue.isTainted(); + replacementStr = replacementValue.toString(); } else { // Replace the match with the replacement string - if (Utf8.isUtf8(replacement)) { + RuntimeScalar replacementValue = stringifyReplacementValue(replacement); + if (Utf8.isUtf8(replacementValue)) { resultNeedsUtf8 = true; } - replacementStr = replacement.toString(); + replacementResultTainted |= taintMode && replacementValue.isTainted(); + replacementStr = replacementValue.toString(); + } + + if (destructiveReplacement + && (inputTainted || patternTainted || replacementResultTainted)) { + string.tainted = true; } if (replacementStr != null) { @@ -2228,21 +2285,30 @@ public static RuntimeBase replaceRegex(RuntimeScalar quotedRegex, RuntimeScalar && retryMatcher.start() == zeroLengthOffset && retryMatcher.end() > zeroLengthOffset) { found++; - updateReplacementMatchState(regex, retryMatcher, inputStr, string); + updateReplacementMatchState(regex, retryMatcher, inputStr, inputValue, captureResultsTainted); String retryReplacementStr; if (replacementIsCode) { RuntimeArray args = (callerArgs != null) ? callerArgs : new RuntimeArray(); RuntimeList result = RuntimeCode.apply(replacement, args, RuntimeContextType.SCALAR); - if (Utf8.isUtf8(result.scalar())) { + RuntimeScalar replacementValue = stringifyReplacementValue(result.scalar()); + if (Utf8.isUtf8(replacementValue)) { resultNeedsUtf8 = true; } - retryReplacementStr = result.toString(); + replacementResultTainted |= taintMode && replacementValue.isTainted(); + retryReplacementStr = replacementValue.toString(); } else { - if (Utf8.isUtf8(replacement)) { + RuntimeScalar replacementValue = stringifyReplacementValue(replacement); + if (Utf8.isUtf8(replacementValue)) { resultNeedsUtf8 = true; } - retryReplacementStr = replacement.toString(); + replacementResultTainted |= taintMode && replacementValue.isTainted(); + retryReplacementStr = replacementValue.toString(); + } + + if (destructiveReplacement + && (inputTainted || patternTainted || replacementResultTainted)) { + string.tainted = true; } if (retryReplacementStr != null) { @@ -2294,18 +2360,26 @@ public static RuntimeBase replaceRegex(RuntimeScalar quotedRegex, RuntimeScalar if (regex.regexFlags.isNonDestructive()) { // /r modifier: return the modified string RuntimeScalar rv = new RuntimeScalar(finalResult); + rv.tainted = inputTainted || patternTainted || replacementResultTainted; if (wasByteString && !resultNeedsUtf8 && !containsWideChars(finalResult)) { rv.type = RuntimeScalarType.BYTE_STRING; } return rv; } else { // Save the modified string back to the original scalar - string.set(finalResult); + RuntimeScalar substitutedValue = new RuntimeScalar(finalResult); + substitutedValue.tainted = inputTainted || patternTainted || replacementResultTainted; if (wasByteString && !resultNeedsUtf8 && !containsWideChars(finalResult)) { - string.type = RuntimeScalarType.BYTE_STRING; + substitutedValue.type = RuntimeScalarType.BYTE_STRING; } + string.set(substitutedValue); + string.tainted = substitutedValue.tainted; // Return the number of substitutions made - return RuntimeScalarCache.getScalarInt(found); + RuntimeScalar count = RuntimeScalarCache.getScalarInt(found); + if (regex.regexFlags.isGlobalMatch() && (inputTainted || patternTainted)) { + count = count.propagateTaint(inputValue, quotedRegex); + } + return count; } } else { if (regex.regexFlags.isNonDestructive()) { @@ -2319,6 +2393,10 @@ public static RuntimeBase replaceRegex(RuntimeScalar quotedRegex, RuntimeScalar } } + private static RuntimeScalar stringifyReplacementValue(RuntimeScalar value) { + return RuntimeScalarType.blessedId(value) != 0 ? Overload.stringify(value) : value; + } + /** * Method to implement Perl's reset() function. * Resets the `matched` flag for each cached regex. @@ -2432,6 +2510,7 @@ public static RuntimeScalar makeMatchResultScalar(String value) { if (lastMatchWasByteString) { scalar.type = RuntimeScalarType.BYTE_STRING; } + scalar.tainted = lastMatchResultsTainted; return scalar; } @@ -2605,7 +2684,8 @@ private static RegexFlags mergeOperationFlags(RegexFlags baseFlags, String modif base.preservesMatch() || operation.preservesMatch(), base.isUnicode(), base.isAscii(), - base.allowEvalGroup() || operation.allowEvalGroup() + base.allowEvalGroup() || operation.allowEvalGroup(), + base.taintResults() || operation.taintResults() ); } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalContext.java b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalContext.java index 79ff7d815..d3b4c935b 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalContext.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalContext.java @@ -20,6 +20,8 @@ public class GlobalContext { private static final ThreadLocal threadTaintMode = ThreadLocal.withInitial(() -> Boolean.FALSE); + private static final ThreadLocal threadJoinTaint = + ThreadLocal.withInitial(() -> Boolean.FALSE); // Special variables internal names public static final String GLOBAL_PHASE = encodeSpecialVar("GLOBAL_PHASE"); // $^GLOBAL_PHASE @@ -35,6 +37,18 @@ public static boolean isTaintModeActive() { return threadTaintMode.get(); } + /** Record the taint state left by join(), used by Perl's legacy taint probe. */ + public static void setThreadJoinTaint(boolean tainted) { + threadJoinTaint.set(tainted); + } + + /** Return and clear the taint state left by the immediately preceding join(). */ + public static boolean consumeThreadJoinTaint() { + boolean tainted = threadJoinTaint.get(); + threadJoinTaint.set(false); + return tainted; + } + // Virtual directory names for JAR-embedded Perl resources // E.g., @INC contains "jar:PERL5LIB", %INC contains "jar:PERL5LIB/DBI.pm" public static final String JAR_PERLLIB = "jar:PERL5LIB"; // maps to /lib/ in JAR @@ -65,7 +79,9 @@ public static void initializeGlobals(CompilerOptions compilerOptions) { // $^S - current state of the interpreter (undef=compiling, 0=not in eval, 1=in eval) GlobalVariable.globalVariables.put("main::" + Character.toString('S' - 'A' + 1), new ScalarSpecialVariable(ScalarSpecialVariable.Id.EVAL_STATE)); - GlobalVariable.getGlobalVariable("main::" + Character.toString('O' - 'A' + 1)).set(SystemUtils.getPerlOsName()); // initialize $^O + GlobalVariable.globalVariables.put( + "main::" + Character.toString('O' - 'A' + 1), + new OperatingSystemVariable(SystemUtils.getPerlOsName())); // initialize $^O GlobalVariable.getGlobalVariable("main::" + Character.toString('V' - 'A' + 1)).set(Configuration.getPerlVersionVString()); // initialize $^V GlobalVariable.getGlobalVariable("main::" + Character.toString('T' - 'A' + 1)).set((int) (System.currentTimeMillis() / 1000)); // initialize $^T to epoch time // Initialize $^W based on -w flag @@ -76,11 +92,16 @@ public static void initializeGlobals(CompilerOptions compilerOptions) { // Initialize $^X - the name used to execute the current copy of Perl // PERLONJAVA_EXECUTABLE is set by the `jperl` or `jperl.bat` launcher String perlExecutable = System.getenv("PERLONJAVA_EXECUTABLE"); + RuntimeScalar executableVariable = + GlobalVariable.getGlobalVariable("main::" + Character.toString('X' - 'A' + 1)); if (perlExecutable != null && !perlExecutable.isEmpty()) { - GlobalVariable.getGlobalVariable("main::" + Character.toString('X' - 'A' + 1)).set(perlExecutable); + executableVariable.set(perlExecutable); } else { // Fallback to "jperl" if environment variable is not set - GlobalVariable.getGlobalVariable("main::" + Character.toString('X' - 'A' + 1)).set("jperl"); + executableVariable.set("jperl"); + } + if (compilerOptions.taintMode) { + executableVariable.tainted = true; } GlobalVariable.getGlobalVariable("main::]").set(Configuration.getPerlVersionOld()); // initialize $] to Perl version @@ -113,6 +134,9 @@ public static void initializeGlobals(CompilerOptions compilerOptions) { if (!GlobalVariable.globalVariables.containsKey("main::0")) { GlobalVariable.getGlobalVariable("main::0").set(compilerOptions.fileName); } + if (compilerOptions.taintMode) { + GlobalVariable.getGlobalVariable("main::0").tainted = true; + } GlobalVariable.getGlobalVariable(GLOBAL_PHASE).set("RUN"); // ${^GLOBAL_PHASE} // ${^TAINT} - set to 1 if -T (taint mode) was specified, 0 otherwise // Only initialize if not already set (to avoid overwriting during re-initialization) @@ -211,6 +235,13 @@ public static void initializeGlobals(CompilerOptions compilerOptions) { env.put(k, envValue); }); + // Command-line arguments are external input just like %ENV and file data. + if (compilerOptions.taintMode) { + for (RuntimeScalar argument : compilerOptions.argumentList.elements) { + argument.tainted = true; + } + } + /* Initialize @INC. @INC Search order mirrors Perl 5's site_perl > core pattern: - "-I" argument (highest priority, user override) diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalRuntimeScalar.java b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalRuntimeScalar.java index 292df9e7e..96827d00b 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalRuntimeScalar.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalRuntimeScalar.java @@ -85,6 +85,9 @@ public void dynamicSaveState() { OutputFieldSeparator.saveInternalOFS(); newLocal = new OutputFieldSeparator(); newLocal.set(RuntimeScalarCache.scalarUndef); + } else if (originalVariable instanceof OperatingSystemVariable) { + newLocal = new OperatingSystemVariable(""); + newLocal.set(RuntimeScalarCache.scalarUndef); } else { newLocal = new GlobalRuntimeScalar(fullName); } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/OperatingSystemVariable.java b/src/main/java/org/perlonjava/runtime/runtimetypes/OperatingSystemVariable.java new file mode 100644 index 000000000..10bc5b732 --- /dev/null +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/OperatingSystemVariable.java @@ -0,0 +1,14 @@ +package org.perlonjava.runtime.runtimetypes; + +/** Perl's $^O: mutable for compatibility, but rejects tainted assignments. */ +public final class OperatingSystemVariable extends RuntimeScalar { + public OperatingSystemVariable(String value) { + super(value); + } + + @Override + public RuntimeScalar set(RuntimeScalar value) { + RuntimeScalar.checkTaint(value, "assigning to $^O"); + return super.set(value); + } +} diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java index a1548dc66..e64595c90 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java @@ -1197,7 +1197,7 @@ private static String autoloadVarFor(RuntimeScalar autoloadCoderef, String looku */ private static void setAutoloadMethodName( String variableName, String fullMethodName, RuntimeScalar methodName) { - RuntimeScalar value = new RuntimeScalar(fullMethodName); + RuntimeScalar value = new RuntimeScalar(fullMethodName).propagateTaint(methodName); if (methodName.type == RuntimeScalarType.BYTE_STRING) { value.type = RuntimeScalarType.BYTE_STRING; } @@ -3191,6 +3191,7 @@ private static RuntimeList dispatchPerlMethodAfterSelfInjected( } String methodName = method.toString(); + RuntimeScalar requestedMethod = method; // Unwrap READONLY_SCALAR for method dispatch. // Constants created via `use constant` with blessed refs go through @@ -3354,7 +3355,7 @@ private static RuntimeList dispatchPerlMethodAfterSelfInjected( String fullMethodName = qualifyAutoloadMethodName(methodName, perlClassName); // Set the $AUTOLOAD variable to the fully qualified name of the method - setAutoloadMethodName(autoloadVariableName, fullMethodName, method); + setAutoloadMethodName(autoloadVariableName, fullMethodName, requestedMethod); } return apply(method, args, callContext); @@ -5014,10 +5015,7 @@ public static void materializeSpecialVarsInResult(RuntimeList result, int callCo RuntimeBase elem = elems.get(i); if (elem instanceof ScalarSpecialVariable ssv) { RuntimeScalar resolved = ssv.getValueAsScalar(); - RuntimeScalar concrete = new RuntimeScalar(); - concrete.type = resolved.type; - concrete.value = resolved.value; - elems.set(i, concrete); + elems.set(i, new RuntimeScalar(resolved)); } else if (!preserveAggregateLvalues && elem instanceof RuntimeArray arr) { // Copy array elements to ensure independence from local restoration. // For tied arrays, use getList() which dispatches through FETCHSIZE/FETCH, @@ -5066,10 +5064,7 @@ public static void materializeSpecialVarsInResult(RuntimeList result, int callCo public static RuntimeScalar materializeBlockResult(RuntimeScalar result) { if (result instanceof ScalarSpecialVariable ssv) { RuntimeScalar resolved = ssv.getValueAsScalar(); - RuntimeScalar concrete = new RuntimeScalar(); - concrete.type = resolved.type; - concrete.value = resolved.value; - return concrete; + return new RuntimeScalar(resolved); } return result; } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java index 209bbbd87..eda04ff30 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java @@ -410,6 +410,9 @@ public RuntimeScalar set(RuntimeScalar value) { // `*foo = \%bar` creates an alias - both names refer to the same hash // Also update all glob aliases if (value.value instanceof RuntimeHash hash) { + if ("main::ENV".equals(this.globName) || "ENV".equals(this.globName)) { + hash.taintEnvironmentAliasDescription = "another variable"; + } GlobalVariable.markPackageGlobalRoot(hash); for (String aliasedName : GlobalVariable.getGlobAliasGroup(this.globName)) { GlobalVariable.globalHashes.put(aliasedName, hash); @@ -654,6 +657,11 @@ public RuntimeScalar set(RuntimeGlob value) { || globName.endsWith("::"); if (sourceHasHash) { RuntimeHash sourceHash = GlobalVariable.getGlobalHash(globName); + if ("main::ENV".equals(this.globName) || "ENV".equals(this.globName)) { + String sourceName = globName.startsWith("main::") + ? globName.substring(6) : globName; + sourceHash.taintEnvironmentAliasDescription = "%" + sourceName; + } GlobalVariable.markPackageGlobalRoot(sourceHash); GlobalVariable.globalHashes.put(this.globName, sourceHash); GlobalVariable.invalidatePackageRootSnapshot(); diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeHash.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeHash.java index d139020b5..21a45a294 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeHash.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeHash.java @@ -32,6 +32,9 @@ public class RuntimeHash extends RuntimeBase implements RuntimeScalarReference, public int type; // Map to store the elements of the hash public Map elements; + // Set when this hash is installed as %ENV through a typeglob alias. + // Perl rejects process execution before inspecting PATH in that case. + public String taintEnvironmentAliasDescription; // Iterator for traversing the hash elements Iterator hashIterator; // Track which keys were stored with BYTE_STRING type (vs STRING/UTF-8). diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java index 593a4edd1..7b93d1db7 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java @@ -111,6 +111,9 @@ private static boolean mightBeInteger(String s) { /** True for the non-reference scalar produced by dereferencing a qr// value. */ public boolean firstClassRegexScalar; + /** Internal provenance used by formline for a picture built with a tainted repeat count. */ + public boolean formatPictureTainted; + /** * True once a string scalar has been used in numeric context. Perl keeps a * numeric slot alongside the string slot on the same SV; this lightweight @@ -393,6 +396,7 @@ public RuntimeScalar(RuntimeScalar scalar) { this.numericLiteralText = scalar.numericLiteralText; this.numericContextSeen = scalar.numericContextSeen; this.firstClassRegexScalar = scalar.firstClassRegexScalar; + this.formatPictureTainted = scalar.formatPictureTainted; if (this.type == GLOBREFERENCE && this.value instanceof RuntimeGlob glob && glob.globName == null) { glob.ioHolderCount++; @@ -473,6 +477,7 @@ public RuntimeScalar(Object value) { this.numericLiteralText = scalar.numericLiteralText; this.numericContextSeen = scalar.numericContextSeen; this.firstClassRegexScalar = scalar.firstClassRegexScalar; + this.formatPictureTainted = scalar.formatPictureTainted; } case Long longValue -> initializeWithLong(longValue); default -> { @@ -581,6 +586,7 @@ private void initializeWithLong(Long value) { this.tainted = false; this.numericContextSeen = false; this.firstClassRegexScalar = false; + this.formatPictureTainted = false; if (value > Integer.MAX_VALUE || value < Integer.MIN_VALUE) { // Java double can only exactly represent integers up to 2^53. // Beyond that, storing as DOUBLE loses precision and breaks exact pack/unpack @@ -1085,6 +1091,41 @@ public RuntimeScalar taintFromExternalInput() { return this; } + /** Mark this scalar tainted when any scalar input to an operation is tainted. */ + public RuntimeScalar propagateTaint(RuntimeScalar... inputs) { + // Outside taint mode there is no provenance to propagate. In + // particular, asking a tied scalar whether it is tainted would invoke + // FETCH a second time after the operator has already fetched its + // value. + if (!GlobalContext.isTaintModeActive()) { + return this; + } + for (RuntimeScalar input : inputs) { + if (input != null && input.isTainted()) { + if (isTainted()) { + return this; + } + // Operator helpers sometimes return a shared scalar-cache value. + // Never attach taint metadata to that shared instance. + RuntimeScalar taintedResult = new RuntimeScalar(this); + taintedResult.tainted = true; + return taintedResult; + } + } + return this; + } + + /** + * Reject a tainted value at a security-sensitive operation. Taint is only + * meaningful while the current Perl program is running with {@code -T}. + */ + public static void checkTaint(RuntimeScalar scalar, String operation) { + if (GlobalContext.isTaintModeActive() && scalar != null && scalar.isTainted()) { + throw new PerlCompilerException( + "Insecure dependency in " + operation + " while running with -T switch"); + } + } + // Add itself to a RuntimeArray. // // ─── WARNING: refCount accounting is intentionally asymmetric here ─── @@ -1331,6 +1372,7 @@ public RuntimeScalar set(RuntimeScalar value) { this.numericLiteralText = value.numericLiteralText; this.numericContextSeen = value.numericContextSeen; this.firstClassRegexScalar = value.firstClassRegexScalar; + this.formatPictureTainted = value.formatPictureTainted; RuntimePosLvalue.invalidatePos(this); } else { this.type = value.type; @@ -1340,6 +1382,7 @@ public RuntimeScalar set(RuntimeScalar value) { this.numericLiteralText = value.numericLiteralText; this.numericContextSeen = value.numericContextSeen; this.firstClassRegexScalar = value.firstClassRegexScalar; + this.formatPictureTainted = value.formatPictureTainted; } refreshSubstrLvalues(); return this; @@ -1407,6 +1450,7 @@ private RuntimeScalar setLarge(RuntimeScalar value) { this.numericLiteralText = null; this.numericContextSeen = false; this.firstClassRegexScalar = false; + this.formatPictureTainted = false; return this; } // Unwrap source special types via switch dispatcher @@ -1448,6 +1492,7 @@ private RuntimeScalar setLarge(RuntimeScalar value) { this.numericLiteralText = value.numericLiteralText; this.numericContextSeen = value.numericContextSeen; this.firstClassRegexScalar = value.firstClassRegexScalar; + this.formatPictureTainted = value.formatPictureTainted; return this; } @@ -1612,6 +1657,7 @@ private RuntimeScalar setLargeRefCounted(RuntimeScalar value) { this.numericLiteralText = value.numericLiteralText; this.numericContextSeen = value.numericContextSeen; this.firstClassRegexScalar = value.firstClassRegexScalar; + this.formatPictureTainted = value.formatPictureTainted; if (this.globalCodeRefFqn != null && this.value instanceof RuntimeCode code) { code.hadStashRef = true; } @@ -1861,6 +1907,7 @@ public RuntimeScalar set(int value) { this.numericLiteralText = null; this.numericContextSeen = false; this.firstClassRegexScalar = false; + this.formatPictureTainted = false; return this; } @@ -1876,6 +1923,7 @@ public RuntimeScalar set(long value) { this.numericLiteralText = null; this.numericContextSeen = false; this.firstClassRegexScalar = false; + this.formatPictureTainted = false; return this; } @@ -1916,6 +1964,7 @@ else if (value.abs().compareTo(BigInteger.valueOf(9007199254740992L)) <= 0) { // this.numericLiteralText = null; this.numericContextSeen = false; this.firstClassRegexScalar = false; + this.formatPictureTainted = false; return this; } @@ -1932,6 +1981,7 @@ public RuntimeScalar set(boolean value) { this.numericLiteralText = null; this.numericContextSeen = false; this.firstClassRegexScalar = false; + this.formatPictureTainted = false; return this; } @@ -1953,6 +2003,7 @@ public RuntimeScalar set(String value) { this.numericLiteralText = null; this.numericContextSeen = false; this.firstClassRegexScalar = false; + this.formatPictureTainted = false; return this; } @@ -2460,6 +2511,7 @@ public RuntimeScalar scalarDeref() { this.numericLiteralText = null; this.numericContextSeen = false; this.firstClassRegexScalar = false; + this.formatPictureTainted = false; yield newScalar; } case REFERENCE -> (RuntimeScalar) value; @@ -2471,7 +2523,7 @@ public RuntimeScalar scalarDeref() { result.type = RuntimeScalarType.STRING; result.value = this.value.toString(); result.firstClassRegexScalar = true; - yield result; + yield result.propagateTaint(this); } case GLOB -> { // Dereferencing a glob as scalar returns the scalar slot @@ -2965,6 +3017,7 @@ public RuntimeScalar undefine() { this.numericLiteralText = null; this.numericContextSeen = false; this.firstClassRegexScalar = false; + this.formatPictureTainted = false; // Invalidate the method resolution cache InheritanceResolver.invalidateCache(); if (releasedCode && WeakRefRegistry.weakRefsExist && !ModuleInitGuard.inModuleInit()) { @@ -2994,6 +3047,7 @@ public RuntimeScalar undefine() { this.numericLiteralText = null; this.numericContextSeen = false; this.firstClassRegexScalar = false; + this.formatPictureTainted = false; // Decrement AFTER clearing (Perl 5 semantics: DESTROY sees the new state) boolean undefOnBlessedWithDestroy = false; @@ -3359,6 +3413,7 @@ public RuntimeScalar preAutoIncrement() { this.numericLiteralText = null; this.numericContextSeen = false; this.firstClassRegexScalar = false; + this.formatPictureTainted = false; // Cases 0-11 are listed in order from RuntimeScalarType, and compile to fast tableswitch switch (type) { case INTEGER -> { // 0 @@ -3466,7 +3521,7 @@ public RuntimeScalar postAutoIncrement() { int intValue = (int) this.value; if (intValue < Integer.MAX_VALUE) { this.value = intValue + 1; - return new RuntimeScalar(intValue); // return old value directly + return new RuntimeScalar(intValue).propagateTaint(this); // return old value directly } } return postAutoIncrementLarge(); @@ -3480,6 +3535,7 @@ private RuntimeScalar postAutoIncrementLarge() { this.numericLiteralText = null; this.numericContextSeen = false; this.firstClassRegexScalar = false; + this.formatPictureTainted = false; // Cases 0-11 are listed in order from RuntimeScalarType, and compile to fast tableswitch switch (type) { @@ -3582,6 +3638,7 @@ public RuntimeScalar preAutoDecrement() { this.numericLiteralText = null; this.numericContextSeen = false; this.firstClassRegexScalar = false; + this.formatPictureTainted = false; // Cases 0-11 are listed in order from RuntimeScalarType, and compile to fast tableswitch switch (type) { case INTEGER -> // 0 @@ -3687,6 +3744,7 @@ public RuntimeScalar postAutoDecrement() { this.numericLiteralText = null; this.numericContextSeen = false; this.firstClassRegexScalar = false; + this.formatPictureTainted = false; // Cases 0-11 are listed in order from RuntimeScalarType, and compile to fast tableswitch switch (type) { @@ -3836,6 +3894,7 @@ public void dynamicSaveState() { currentState.numericLiteralText = this.numericLiteralText; currentState.numericContextSeen = this.numericContextSeen; currentState.firstClassRegexScalar = this.firstClassRegexScalar; + currentState.formatPictureTainted = this.formatPictureTainted; // Push the current state onto the stack dynamicStateStack.push(currentState); // Clear the current type and value @@ -3847,6 +3906,7 @@ public void dynamicSaveState() { this.numericLiteralText = null; this.numericContextSeen = false; this.firstClassRegexScalar = false; + this.formatPictureTainted = false; } /** @@ -3880,6 +3940,7 @@ public void dynamicRestoreState() { this.numericLiteralText = previousState.numericLiteralText; this.numericContextSeen = previousState.numericContextSeen; this.firstClassRegexScalar = previousState.firstClassRegexScalar; + this.formatPictureTainted = previousState.formatPictureTainted; releaseScalarReferenceContents(scalarReferenceContents); diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeSubstrLvalue.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeSubstrLvalue.java index 5c6724a40..d32b10065 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeSubstrLvalue.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeSubstrLvalue.java @@ -129,6 +129,15 @@ public RuntimeScalar set(RuntimeScalar value) { // Update the parent RuntimeScalar with the modified string RuntimeScalar updated = new RuntimeScalar(updatedValue.toString()); + // Assignment through substr is an in-place mutation. Perl preserves + // existing taint on the target and also propagates taint from the + // replacement value. + if (GlobalContext.isTaintModeActive()) { + // The proxy cached its parent's provenance when it was created; + // consulting the tied parent again here would perform another + // FETCH for the same lvalue operation. + updated.tainted = this.tainted || value.isTainted(); + } // Preserve BYTE_STRING type: if the parent was a byte string and the replacement // doesn't introduce UTF-8 characters, keep the result as BYTE_STRING. // In Perl, substr assignment on a byte string with a byte replacement stays bytes. @@ -167,6 +176,7 @@ public RuntimeScalar set(RuntimeScalar value) { this.length = replacementLength; this.type = value.type; this.value = newValue; + this.tainted = updated.tainted; return this; } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/ScalarSpecialVariable.java b/src/main/java/org/perlonjava/runtime/runtimetypes/ScalarSpecialVariable.java index 0fadba55f..c6e50d30d 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/ScalarSpecialVariable.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/ScalarSpecialVariable.java @@ -221,7 +221,7 @@ public RuntimeScalar getValueAsScalar() { } case LAST_PAREN_MATCH -> { String lastCapture = RuntimeRegex.lastCaptureString(); - yield lastCapture != null ? new RuntimeScalar(lastCapture) : scalarUndef; + yield lastCapture != null ? makeRegexResultScalar(lastCapture) : scalarUndef; } case LAST_SUCCESSFUL_PATTERN -> RuntimeRegex.lastSuccessfulPattern != null ? new RuntimeScalar(RuntimeRegex.lastSuccessfulPattern) : scalarUndef; @@ -500,6 +500,7 @@ private static RuntimeScalar makeRegexResultScalar(String value) { if (RuntimeRegex.lastMatchWasByteString) { scalar.type = RuntimeScalarType.BYTE_STRING; } + scalar.tainted = RuntimeRegex.lastMatchResultsTainted; return scalar; } diff --git a/src/test/resources/unit/taint_mode.t b/src/test/resources/unit/taint_mode.t new file mode 100644 index 000000000..d204ca7d5 --- /dev/null +++ b/src/test/resources/unit/taint_mode.t @@ -0,0 +1,451 @@ +#!perl -T +use strict; +use warnings; +use feature 'switch'; +no warnings 'experimental::smartmatch'; +use Fcntl qw(O_RDONLY O_WRONLY); +use Scalar::Util qw(tainted); +use Test::More; + +my $empty_taint = substr($^X, 0, 0); +my $text = "abc$empty_taint"; + +ok(${^TAINT}, '-T enables ${^TAINT}'); +ok(tainted($^X), '$^X is tainted'); +ok(tainted($ENV{PATH}), '%ENV values are tainted'); +ok(!tainted($^O), '$^O is not tainted'); +{ + local $^O; + my $os_assignment_ok = eval { $^O = $^X; 1 }; + ok(!$os_assignment_ok, '$^O rejects a tainted assignment'); + like($@, qr/^Insecure dependency in assigning to \$\^O while running with -T switch/, + '$^O assignment reports the Perl security error'); +} +ok(tainted($empty_taint), 'substr preserves source taint'); +ok(tainted($text), 'concatenation propagates taint'); + +my $substr_target = $ENV{PATH}; +substr($substr_target, 0) = 'replacement'; +ok(tainted($substr_target), 'lvalue substr assignment preserves target taint'); + +my $utf8_taint = "ascii$empty_taint"; +utf8::encode($utf8_taint); +ok(tainted($utf8_taint), 'utf8::encode preserves taint'); +utf8::decode($utf8_taint); +ok(tainted($utf8_taint), 'utf8::decode preserves taint'); +utf8::upgrade($utf8_taint); +ok(tainted($utf8_taint), 'utf8::upgrade preserves taint'); +utf8::downgrade($utf8_taint); +ok(tainted($utf8_taint), 'utf8::downgrade preserves taint'); + +{ + package TaintFetchProbe; + sub TIESCALAR { bless { value => $_[1], fetches => 0 }, $_[0] } + sub FETCH { ++$_[0]{fetches}; $_[0]{value} } + sub STORE { $_[0]{value} = $_[1] } + + package main; + tie my $tied_substitution, 'TaintFetchProbe', "abc$empty_taint"; + my $probe = tied $tied_substitution; + $tied_substitution =~ s/a/x/; + is($probe->{fetches}, 1, 'tainted substitution fetches a tied target once'); +} + +my $copy = $text; +ok(tainted($copy), 'assignment propagates taint'); +{ + no warnings 'numeric'; + ok(tainted(1 + $empty_taint), 'addition propagates taint'); + ok(tainted(2 * (1 + $empty_taint)), 'multiplication propagates taint'); +} +ok(tainted(length($text)), 'length propagates taint'); +ok(!tainted(length('abc')), 'taint propagation does not contaminate cached clean scalars'); +ok(tainted(uc($text)), 'case conversion propagates taint'); +ok(tainted(ord($text)), 'ord propagates taint'); + +my $tainted_counter = 1 + (0 + $empty_taint); +my $old_counter = $tainted_counter++; +ok(tainted($old_counter), 'post-increment result preserves input taint'); +ok(tainted($tainted_counter), 'post-increment lvalue remains tainted'); +$old_counter = $tainted_counter--; +ok(tainted($old_counter), 'post-decrement result preserves input taint'); +ok(tainted($tainted_counter), 'post-decrement lvalue remains tainted'); + +$text =~ /^(.*)$/; +ok(!tainted($1), 'regex capture untaints validated input'); + +my $tainted_pattern = "(abc)$empty_taint"; +'abc' =~ /$tainted_pattern/; +ok(tainted($1), 'a tainted regex pattern taints its capture'); +ok(tainted(qr/$tainted_pattern/), 'qr preserves pattern taint'); +my $tainted_regex_ref = qr/(.)$empty_taint/; +my $bare_tainted_regex = $$tainted_regex_ref; +my $bare_regex_target = 'abc'; +$bare_regex_target =~ s/$bare_tainted_regex/x/; +ok(tainted($bare_tainted_regex), 'dereferencing qr preserves regex taint'); +ok(tainted($bare_regex_target), + 'substitution with a dereferenced tainted regex taints the target'); + +{ + use re 'taint'; + $text =~ /^(.*)$/; + ok(tainted($1), q{use re 'taint' preserves input taint in captures}); + + my $capture = sub { $_[0] =~ /^(.*)$/; $1 }; + ok(tainted($capture->($text)), 'capture returned from a sub preserves taint'); + ok(!tainted($capture->('clean')), 'capture taint does not stick to later matches'); +} + +my $sub_source = "abcd$empty_taint"; +my $sub_count = $sub_source =~ s/(.+)/xyz/; +ok(tainted($sub_source), 'substitution preserves source taint on the target'); +ok(!tainted($sub_count), 'single substitution count stays clean'); +ok(!tainted($1), 'ordinary substitution capture untaints input'); + +$sub_source = "abcd$empty_taint"; +$sub_count = $sub_source =~ s/(.)/x/g; +ok(tainted($sub_count), 'global substitution count depends on tainted input'); + +my $sub_pattern = "(.+)$empty_taint"; +my $sub_target = 'abcd'; +$sub_count = $sub_target =~ s/$sub_pattern/xyz/; +ok(tainted($sub_target), 'tainted substitution pattern taints the target'); +ok(!tainted($sub_count), 'single substitution count ignores pattern taint'); +ok(tainted($1), 'tainted substitution pattern taints captures'); + +my $sub_replacement = "xyz$empty_taint"; +$sub_target = 'abcd'; +$sub_count = $sub_target =~ s/(.+)/$sub_replacement/; +ok(tainted($sub_target), 'tainted replacement taints the substitution target'); +ok(!tainted($sub_count), 'substitution count ignores replacement taint'); + +$sub_target = 'abcd'; +my $sub_copy = $sub_target =~ s/(.+)/$sub_replacement/r; +ok(!tainted($sub_target), 'non-destructive substitution leaves its source clean'); +ok(tainted($sub_copy), 'non-destructive substitution preserves replacement taint'); + +{ + package TaintStringify; + use overload '""' => sub { $_[0]->[0] }; + sub new { bless [$_[1]], $_[0] } +} + +my $tainted_object = TaintStringify->new("object$empty_taint"); +ok(tainted("$tainted_object"), 'stringification overload preserves result taint'); +ok(tainted("prefix$tainted_object"), 'mixed interpolation preserves overload result taint'); +$sub_target = 'abcd'; +$sub_target =~ s/(.+)/prefix$tainted_object/; +ok(tainted($sub_target), 'interpolated overloaded replacement preserves taint'); +$sub_target = 'abcd'; +$sub_copy = $sub_target =~ s/(.+)/$tainted_object/r; +ok(tainted($sub_copy), 'whole overloaded replacement preserves taint'); + +{ + use re 'taint'; + $sub_source = "abcd$empty_taint"; + $sub_source =~ s/(.+)/xyz/; + ok(tainted($1), q{use re 'taint' taints substitution captures from tainted input}); +} + +sub perlsec_tainted { + return !eval { no warnings; join('', @_), kill 0; 1 }; +} + +ok(perlsec_tainted($text), 'legacy perlsec join/kill probe detects taint'); +ok(!perlsec_tainted('clean'), 'legacy perlsec join/kill probe accepts clean data'); + +my $eval_ok = eval { eval $text; 1 }; +ok(!$eval_ok, 'tainted eval STRING is rejected'); +like($@, qr/^Insecure dependency in eval while running with -T switch/, + 'tainted eval reports the Perl security error'); + +{ + local @ENV{qw(PATH IFS CDPATH ENV BASH_ENV)}; + $ENV{PATH} = '/usr/bin'; + delete @ENV{qw(IFS CDPATH ENV BASH_ENV)}; + + my $system_ok = eval { system("/bin/echo$empty_taint", 'unused'); 1 }; + ok(!$system_ok, 'system rejects a tainted command'); + like($@, qr/^Insecure dependency in system while running with -T switch/, + 'system reports the Perl security error'); + + my $qx_ok = eval { qx{/bin/echo$empty_taint unused}; 1 }; + ok(!$qx_ok, 'qx rejects a tainted command'); + like($@, qr/^Insecure dependency in `` while running with -T switch/, + 'qx reports the Perl security error'); + + local $ENV{PATH} = '.'; + my $path_ok = eval { qx{/bin/echo path-check}; 1 }; + ok(!$path_ok, 'process launch rejects a relative PATH directory'); + like($@, qr/Insecure directory in \$ENV\{PATH\}/, + 'relative PATH reports the Perl security error'); + + local $ENV{PATH} = '/usr/bin'; + local $ENV{TERM} = 'dumb'; + my $command_output = qx{/bin/echo external-output}; + ok(tainted($command_output), 'command output enters Perl as tainted data'); + + local $ENV{TERM} = "unsafe=$empty_taint"; + my $term_ok = eval { qx{/bin/echo term-check}; 1 }; + ok(!$term_ok, 'process launch rejects tainted TERM metacharacters'); + like($@, qr/Insecure \$ENV\{TERM\}/, + 'tainted TERM reports the Perl security error'); +} + +ok(tainted($0), 'the program name is tainted under taint mode'); + +{ + local $^A = ''; + formline '@<<<<', $text; + ok(tainted($^A), 'tainted formline argument taints the accumulator'); + $^A = ''; + formline '@<<<<' . $empty_taint, 'clean'; + ok(tainted($^A), 'tainted formline picture taints the accumulator'); + $^A = $empty_taint; + formline '@<<<<', 'clean'; + ok(tainted($^A), 'formline preserves existing accumulator taint'); + my $tainted_width = 5 + (0 + $empty_taint); + my $dynamic_picture = '@' . ('<' x $tainted_width); + ok(tainted($dynamic_picture), 'dynamic picture composition exposes repeat-count taint'); + $^A = ''; + formline $dynamic_picture, 'clean'; + ok(tainted($^A), 'tainted repeat count marks a dynamic formline picture'); +} + +{ + open my $ioctl_fh, '<', $^X or die $!; + my $ioctl_ok = eval { ioctl $ioctl_fh, 0 + $empty_taint, "x$empty_taint"; 1 }; + ok(!$ioctl_ok, 'ioctl rejects tainted control arguments'); + like($@, qr/^Insecure dependency in ioctl while running with -T switch/, + 'ioctl reports the Perl security error'); + close $ioctl_fh; +} + +{ + open my $source, '<', $^X or die $!; + local $/; + my $contents = <$source>; + my $eof = <$source>; + ok(tainted($contents), 'file input enters Perl as tainted data'); + ok(tainted($eof), 'undef returned at file EOF retains input provenance'); + close $source; +} + +my $tainted_path = "/tmp/perlonjava-taint-no-such-$$" . $empty_taint; +my @file_operations = ( + [ unlink => sub { unlink $tainted_path } ], + [ mkdir => sub { mkdir $tainted_path } ], + [ rmdir => sub { rmdir $tainted_path } ], + [ chdir => sub { chdir $tainted_path } ], + [ rename => sub { rename $tainted_path, "$tainted_path-new" } ], + [ link => sub { link $tainted_path, "$tainted_path-link" } ], + [ symlink => sub { symlink $tainted_path, "$tainted_path-symlink" } ], + [ chmod => sub { chmod 0600, $tainted_path } ], + [ require => sub { require $tainted_path } ], + [ do => sub { do $tainted_path } ], +); + +for my $case (@file_operations) { + my ($operation, $code) = @$case; + my $ok = eval { $code->(); 1 }; + ok(!$ok, "$operation rejects a tainted path"); + like($@, + qr/^Insecure dependency in $operation while running with -T switch/, + "$operation reports the Perl security error"); +} + +my $read_open_ok = eval { open my $fh, '<', $tainted_path; 1 }; +ok($read_open_ok, 'three-argument open permits a tainted read path'); +my $write_open_ok = eval { open my $fh, '>', $tainted_path; 1 }; +ok(!$write_open_ok, 'three-argument open rejects a tainted write path'); +like($@, qr/^Insecure dependency in open while running with -T switch/, + 'write open reports the Perl security error'); + +my $sysread_open_ok = eval { sysopen my $fh, $tainted_path, O_RDONLY; 1 }; +ok($sysread_open_ok, 'sysopen permits a tainted read-only path'); +my $missing_sysopen = sysopen my $missing_fh, $tainted_path, O_RDONLY; +ok(!defined($missing_sysopen), 'failed read-only sysopen returns undef'); +my $syswrite_open_ok = eval { sysopen my $fh, $tainted_path, O_WRONLY; 1 }; +ok(!$syswrite_open_ok, 'sysopen rejects a tainted write path'); +like($@, qr/^Insecure dependency in sysopen while running with -T switch/, + 'sysopen reports the Perl security error'); + +{ + no warnings 'numeric'; + my $tainted_zero = 0 + $empty_taint; + ok(tainted(O_WRONLY | $tainted_zero), 'bitwise flags preserve taint'); + my $tainted_read_mode = O_RDONLY | $tainted_zero; + my $tainted_read_ok = eval { + sysopen my $fh, '/tmp/perlonjava-taint-no-such', $tainted_read_mode; + 1; + }; + ok($tainted_read_ok, 'sysopen permits tainted read-only flags'); + my $tainted_write_mode = O_WRONLY | $tainted_zero; + my $tainted_write_ok = eval { + sysopen my $fh, '/tmp/perlonjava-taint-no-such', $tainted_write_mode; + 1; + }; + ok(!$tainted_write_ok, 'sysopen rejects tainted write flags'); + like($@, qr/^Insecure dependency in sysopen while running with -T switch/, + 'tainted sysopen flags report the Perl security error'); + for my $case ( + [ truncate => sub { truncate '/tmp/perlonjava-taint-no-such', $tainted_zero } ], + [ utime => sub { utime $tainted_zero, $tainted_zero, '/tmp/perlonjava-taint-no-such' } ], + [ chown => sub { chown -1, -1, $tainted_path } ], + ) { + my ($operation, $code) = @$case; + my $ok = eval { $code->(); 1 }; + ok(!$ok, "$operation rejects tainted arguments"); + like($@, qr/^Insecure dependency in $operation while running with -T switch/, + "$operation reports the Perl security error"); + } +} + +for (qw(x y z)) { + my $outer_topic = $_; + my $letter = "$_$empty_taint"; + my $result = do { + no warnings 'deprecated'; + given ($_) { + when ('x') { $letter } + when ('y') { goto leave_given } + default { $letter } + leave_given: $letter + } + }; + is($result, $letter, "given preserves the result for $outer_topic"); + ok(tainted($result), "given preserves taint for $outer_topic"); + is($_, $outer_topic, "given restores the foreach topic for $outer_topic"); +} + +my @split = split /!/, "left!right$empty_taint"; +ok(tainted($split[0]) && tainted($split[1]), 'split propagates input taint'); + +{ + no warnings 'numeric'; + ok(tainted(~("abc$empty_taint")), 'bitwise complement propagates taint'); +} +ok("M$empty_taint" ~~ ['m', 'M'], + 'tainted scalar smartmatches a later array element'); +ok(!("M$empty_taint" ~~ ['m', undef]), + 'tainted scalar smartmatch handles undef array elements'); +ok(tainted(crypt('secret', "aa$empty_taint")), 'crypt propagates salt taint'); +ok(tainted(vec("A$empty_taint", 0, 8)), 'vec propagates source taint'); +ok(tainted(pack('a*', "packed$empty_taint")), 'pack propagates value taint'); + +ok(tainted(sprintf('%s', "formatted$empty_taint", 'clean')), + 'sprintf propagates used argument taint'); +{ + no warnings 'numeric'; + my $tainted_zero = 0 + $empty_taint; + ok(!tainted(sprintf('%s', 'clean', $tainted_zero)), + 'sprintf ignores an unused tainted numeric argument'); +} + +my $tainted_format = "%s$empty_taint"; +my $sprintf_ok = eval { sprintf($tainted_format, 'value'); 1 }; +ok(!$sprintf_ok, 'sprintf rejects a tainted format'); +like($@, qr/^Insecure dependency in sprintf while running with -T switch/, + 'sprintf reports the Perl security error'); + +my $printf_ok = eval { printf($tainted_format, 'value'); 1 }; +ok(!$printf_ok, 'printf rejects a tainted format'); +like($@, qr/^Insecure dependency in printf while running with -T switch/, + 'printf reports the Perl security error'); + +{ + local $ENV{PATH} = '/usr/bin'; + for my $case ( + [ 'system PROGRAM LIST without comma', + sub { system $empty_taint 'clean-argument' } ], + [ 'system { PROGRAM } LIST', + sub { system { 'clean-program' } $empty_taint } ], + ) { + my ($description, $operation) = @$case; + my $ok = eval { $operation->(); 1 }; + ok(!$ok, "$description rejects tainted command data"); + like($@, qr/^Insecure dependency in system while running with -T switch/, + "$description reports the Perl security error"); + } +} + +{ + our %taint_env_alias = (PATH => '/usr/bin'); + { + local *ENV = \%taint_env_alias; + my $ok = eval { system 'taint-command-that-does-not-exist'; 1 }; + ok(!$ok, 'process execution rejects %ENV aliased through a hash reference'); + like($@, qr/^%ENV is aliased to another variable while running with -T switch/, + 'hash-reference %ENV alias reports the Perl security error'); + } + { + local *ENV = *taint_env_alias; + my $ok = eval { system 'taint-command-that-does-not-exist'; 1 }; + ok(!$ok, 'process execution rejects %ENV aliased through a typeglob'); + like($@, qr/^%ENV is aliased to %taint_env_alias while running with -T switch/, + 'typeglob %ENV alias reports the Perl security error'); + } +} + +{ + package TaintStoreProbe; + our @seen; + sub TIEARRAY { bless {}, shift } + sub TIEHASH { bless {}, shift } + sub STORE { + push @seen, [ + Scalar::Util::tainted($_[1]), + Scalar::Util::tainted($_[2]), + ]; + } + + package main; + my $key = "1$empty_taint"; + my $value = "value$empty_taint"; + tie my @tied_array, 'TaintStoreProbe'; + tie my %tied_hash, 'TaintStoreProbe'; + $tied_array[$key] = $value; + $tied_hash{$key} = $value; + ok($TaintStoreProbe::seen[0][0] && $TaintStoreProbe::seen[0][1], + 'tied array STORE receives tainted key and value'); + ok($TaintStoreProbe::seen[1][0] && $TaintStoreProbe::seen[1][1], + 'tied hash STORE receives tainted key and value'); +} + +{ + package TaintAutoloadProbe; + our @seen; + sub AUTOLOAD { + our $AUTOLOAD; + return if $AUTOLOAD =~ /DESTROY/; + push @seen, Scalar::Util::tainted($AUTOLOAD); + } + + package main; + my $object = bless {}, 'TaintAutoloadProbe'; + my $tainted_method = "tainted_method$empty_taint"; + $object->$tainted_method; + $object->clean_method; + ok($TaintAutoloadProbe::seen[0], + 'AUTOLOAD name inherits taint from a dynamic method name'); + ok(!$TaintAutoloadProbe::seen[1], + 'AUTOLOAD name is reset to clean for a clean method name'); +} + +sub IsTaintProbeA { '0041' } +my $tainted_property = "IsTaintProbeA$empty_taint"; +my $property_ok = eval { 'A' =~ /\p{$tainted_property}/; 1 }; +ok(!$property_ok, 'tainted user-defined regex property is rejected'); +like($@, qr/^Insecure user-defined property "IsTaintProbeA" in regex/, + 'tainted user-defined property reports the Perl security error'); + +{ + use re 'eval'; + my $tainted_code_pattern = "(?{})$empty_taint"; + my $code_pattern_ok = eval { 'a' =~ /$tainted_code_pattern/; 1 }; + ok(!$code_pattern_ok, 'tainted runtime regex code block is rejected'); + like($@, qr/^Eval-group in insecure regular expression/, + 'tainted regex code block reports the Perl security error'); +} + +done_testing; diff --git a/src/test/resources/unit/tie_scalar.t b/src/test/resources/unit/tie_scalar.t index af2ecc2ce..7be835c68 100644 --- a/src/test/resources/unit/tie_scalar.t +++ b/src/test/resources/unit/tie_scalar.t @@ -124,6 +124,22 @@ subtest 'FETCH operations' => sub { is($obj->{fetch_count}, 6, 'FETCH called in numeric context'); }; +subtest 'operators fetch once' => sub { + my $scalar; + my $obj = tie $scalar, 'TiedScalar'; + $scalar = 12; + + my $negative = -$scalar; + is($negative, -12, 'unary minus uses the tied value'); + is($obj->{fetch_count}, 1, 'unary minus calls FETCH once'); + + $obj->{fetch_count} = 0; + my $quotient = $scalar / 3; + is($quotient, 4, 'division uses the tied value'); + is($obj->{fetch_count}, 1, 'division calls FETCH once'); + +}; + subtest 'STORE operations' => sub { my $scalar; my $obj = tie $scalar, 'TiedScalar';